830 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
Gent S
82d5455da6 Ignore race rewards for EPs that aren't found 2020-12-23 14:53:56 -05:00
Gent S
52389c2c69 Make code redemption case-insensitive 2020-12-23 13:50:12 -05:00
Gent S
ae75324153 Fix duplicate tasks on instanced mission failure 2020-12-23 13:50:12 -05:00
Gent S
a1a5815f1f Fix academy nano missions not going into the 0th slot 2020-12-23 13:50:12 -05:00
Gent S
772f80188e Prevent duplicate running tasks from loading into the Player struct 2020-12-23 13:50:12 -05:00
kamilprzyb
f28c643b48 Allow permanent vehicles 2020-12-23 13:50:12 -05:00
Kamil
5f82658c8d adjusted item codes logic to handle multiple items per code 2020-12-23 13:50:12 -05:00
ff75aa6693 Add the Academy build to the Appveyor script 2020-12-23 13:50:12 -05:00
50b2bdcb16 Fixed Nano Style Issues 2020-12-23 13:50:12 -05:00
Kamil
34dbb59fb1 Implement redeem codes 2020-12-23 13:50:12 -05:00
Kamil
a74c9be2ff Implement Nano capsules 2020-12-23 13:50:12 -05:00
fcdea2e723 Implement multiple-choice mission rewards 2020-12-23 13:50:12 -05:00
90191fd494 Add academy.json loading to TableData
This is where the permanent Academy-exclusive NPCs and mobs will be
loaded from. Resurrect 'Ems work now.
2020-12-23 13:50:12 -05:00
Gent S
effbbd9a5e Fix level-up not triggering and using wrong packets 2020-12-23 13:50:12 -05:00
bc7f4883a2 Add Academy defaults to the config file 2020-12-23 13:50:12 -05:00
e0808ffcbd Read the NPC ID limit from the XDT 2020-12-23 13:50:12 -05:00
9057f31bff Implement Academy level-up 2020-12-23 13:50:12 -05:00
0a9f637123 Do not give the Lightning Gun and Buttercup Nano to new Academy players
And do not mark the two missions from the old Tutorial as completed.
2020-12-23 13:50:12 -05:00
027f513a23 Save Academy Nanos to DB 2020-12-23 13:50:12 -05:00
a99f95d15f Initial Academy support
Support for the Academy's Nano system is complete, but they're not being
saved to the DB yet.
2020-12-23 13:50:12 -05:00
Gent S
49158360ca Update tdata ref 2020-12-23 13:48:19 -05:00
0104bc9329 Enemy ability tweaks
- Battery drain does not set your battery below 0.
- Debuffs cast by enemies take into account intensity now. (Snare will work)
- Used int_max to replace arbitrary 20000, added more comments.
2020-12-22 12:12:02 -05:00
4fc5c092f8 Attack logic and various balance changes
- combatStep was altered, the mob can attack and give chase at the same time. Kiting melee mobs around is much harder.
- Mobs in general are more harder, closer to how it was in retro.
- Nanos styles are less detrimental to battle.
2020-12-22 11:30:57 -05:00
8d1bc94b7e Mission Handling Fixes
- Q. Item count glitches are dealt with
- Warping out now correctly rewinds missions.
2020-12-22 11:30:57 -05:00
322e354f5b Fix indentation in RacingManager.cpp and compilation on Linux 2020-12-22 14:46:09 +01:00
Gent S
8a2073d081 Fix racing timestamps 2020-12-22 01:04:46 -05:00
Gent S
e915c54ed0 Correctly set time limit, rank, and reward 2020-12-22 00:37:04 -05:00
Gent S
e953b51229 Add reward tables for racing 2020-12-22 00:36:39 -05:00
Gent S
b9013149f3 Racing cleanup 2020-12-21 21:02:19 -05:00
Gent S
e6da454c73 Finish racing DB integration 2020-12-21 16:18:08 -05:00
Gent S
598c7ce1d0 Add racing-related DB functions 2020-12-21 16:18:08 -05:00
Gent S
ea47f67b2b Properly utilize instance data in map info packet 2020-12-21 16:18:08 -05:00
Gent S
91ea8be72e Implement basic race handling 2020-12-21 16:18:08 -05:00
Gent S
b81a3761b6 Load instance data from XDT 2020-12-21 16:18:08 -05:00
Gent S
45b8f8f581 Add RacingManager 2020-12-21 16:18:08 -05:00
Gent S
cfc6a94c7a Include sql directory and config.ini in AppVeyor artifacts 2020-12-21 16:02:58 -05:00
c3d9883ddb Fix migration on Linux 2020-12-20 23:09:10 +01:00
Gent S
ea12ec9607 Use binary streams to make DB backups, since filesystem has low compat 2020-12-20 15:52:34 -05:00
Gent S
2e173df2ca DB v2 Tweaks 2020-12-19 21:17:07 -05:00
Kamil
1fb48536c2 created external sql file for tables and 1st migration 2020-12-19 20:00:47 -05:00
Kamil
002bfffb62 implemented db migration system 2020-12-19 19:15:14 -05:00
Kamil
140227406c implemented custom error messages in login screen for regex fail or banned account 2020-12-19 19:15:14 -05:00
26d0623d07 Fix Database::sendEmail()
* Do not define an sqlite_stmt* named stmt in a loop such that it shadows
an outer variable of the same name.
* Do not re-prepare a statement that has been reset.
2020-12-19 05:51:06 +01:00
f8a359dfe9 Do not truncate emails to 127 characters
And assert that we never supply a string long enough for
sanitizeText() to truncate.
2020-12-19 05:51:06 +01:00
Gent S
2f44243abb Please.... please take the slider.... 2020-12-18 20:48:09 -05:00
e3561e9d15 Change the default ports to match OG and Retro
The default monitor port can stay on 8003 for now.
2020-12-18 00:24:24 +01:00
80dabf4406 Finalize ALL the statements
* Fixed not being able to modify the DB externally while the script is
running
* Made most DB-related errors print the appropriate error string
* Fixed the settings looking for dbsaveinterval in the shard category
instead of the login category
* Added -shr and -wal to .gitignore (even though we're not actually
using WAL mode)

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

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

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

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

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

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

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

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

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

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

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

fixed stamina bug and improved sanity check

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

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

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

Implemented two more mission types + Tweaks

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

* Mobs now drop correct crates

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

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

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

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

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

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

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

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

* Added more nano powers

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

* Further Nano powers + Bugfixes

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

* Dismiss nano when starting a MSS ride

* Sneak, Invisibility and Bugfixes

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

* Group PR cleanup.

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

Didn't have the opportunity to actually test groups.

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

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

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

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

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

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

* added gumballs functionality

* added nano potions functionality

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

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

* implemented saving mentor

* moved int64->blob parsing to a separate function

* moved parsing blob->int64 to a separate function

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

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

* WIP saving quest items and active tasks

* Quest items are stored in inventory table instead of blob

* added sanity check for missionId

* saving active missions works

* removed unneccesary include

* implemented saving warplocationflag, skywaylocationflag and currentmissionid in database

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

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

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

* adjusted saving active missions

* removed blob parsing functions that ended up being unused

* removed accidentaly added include

* removed sending PCStyle2 on Player Enter

* added a sanity check in itemMoveHandler

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

* set BuddyWarpCooldown to 60s on PCLoad

* fixed a bug causing EXIT DUPLICATE not working

* added creation and last login timestamps to accounts and players

* added a sanity check for P_CL2LS_REQ_PC_EXIT_DUPLICATE

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

* add usewebapi to config

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


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

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

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

Co-authored-by: dongresource <dongresource@protonmail.com>
2020-09-11 23:19:03 +02:00
468840c9ea Fixed gcc vs clang preprocessor tomfoolery. 2020-09-11 00:19:21 +02:00
52f02168bc Implemented quest item cleanup and fixed a few bugs. 2020-09-11 00:19:21 +02:00
ddb5f782b7 Refactored mission data and implemented quest item mob drops.
Most future missions are now playable.
Quest items aren't being properly cleaned up yet.
2020-09-11 00:19:21 +02:00
3665dc2c93 [WIP] Incremental mission save 2
This commit (and the previous one) exist to document the first approach I
took to storing mission data. It's only here for posterity. This comment
was added while rebasing.
2020-09-11 00:19:19 +02:00
ae654f996c [WIP] Incremental mission save 1
This commit (and the next one) exist to document the first approach I
took to storing mission data. It's only here for posterity. This comment
was added while rebasing.
2020-09-11 00:19:16 +02:00
e33b7f20e9 [bugfix] Preserve Taros and FM when opening Crates. 2020-09-11 00:08:26 +02:00
5b49e71de7 Implemented mission rewards.
Might need to refactor item drops, especially after implementing
task-result quest item drops.
2020-09-11 00:08:26 +02:00
3172724596 Implemented the mob kill counter in missions and fixed a bug. 2020-09-11 00:08:26 +02:00
8887c6349b Add support for cookie-based login back in. 2020-09-11 00:08:26 +02:00
CakeLancelot
6e0b101a76 Merge pull request #78 from JadeShrineMaiden/additions2
Levelling up added to /nano
2020-09-10 12:02:28 -05:00
29cde56fb1 Levelling up added to /nano
- Using /nano levels up Player, a player cannot level down.
2020-09-10 17:51:52 +01:00
e65f07780b Merge branch 'master' of https://github.com/OpenFusionProject/OpenFusion 2020-09-09 12:06:35 -05:00
eb1ad6bb37 switched to dumped XDT & moved to a submodule 2020-09-09 12:06:22 -05:00
CakeLancelot
e409b8bb39 Add .gitattributes file, exclude src/contrib from linguist (#73) 2020-09-08 19:55:59 -04:00
45a33758a5 Account for the size of packet length and ID in validation functions. 2020-09-08 03:06:55 +02:00
FinnHornhoover
266fddbffa fixed warnings and compile errors for msys2 2020-09-08 03:41:13 +03:00
e90ae10746 Summoned NPC are now visible to other players. 2020-09-07 22:12:53 +02:00
b797993014 updated readme 2020-09-07 13:07:04 -05:00
147 changed files with 24319 additions and 260198 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

5
.gitattributes vendored Normal file
View File

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

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

5
.gitignore vendored
View File

@@ -11,3 +11,8 @@ build/
.vs/
.idea/
*.db
*-shm
*-wal
version.h
infer-out
gmon.out

3
.gitmodules vendored Normal file
View File

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

View File

@@ -3,6 +3,8 @@ project(OpenFusion)
set(CMAKE_CXX_STANDARD 17)
execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
# OpenFusion supports multiple packet/struct versions
# 104 is the default version to build which can be changed
# For example: cmake -B build -DPROTOCOL_VERSION=728
@@ -31,14 +33,24 @@ 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)
file(GLOB_RECURSE SOURCES src/**.[ch]pp vendor/**.[ch]pp vendor/**.[ch] version.h)
configure_file(version.h.in ${CMAKE_SOURCE_DIR}/version.h @ONLY)
add_executable(openfusion ${SOURCES})
set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME})
# find sqlite3 and use it
find_package(sqlite3 REQUIRED)
target_include_directories(openfusion PRIVATE ${SQLite3_INCLUDE_DIRS})
target_link_libraries(openfusion PRIVATE ${SQLite3_LIBRARIES})
# Makes it so config, tdata, etc. get picked up when starting via the debugger in VS
set_property(TARGET openfusion PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
# Use pthreads if not generating a VS solution or MinGW makefile (because MinGW will prefer Win32 threads)
# Checking if the compiler ID is MSVC will allow us to open the project as a CMake project in VS.
# It's not something you should do, but it's there if you need it...

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

161
Makefile
View File

@@ -1,9 +1,12 @@
GIT_VERSION!=git describe --tags
CC=clang
CXX=clang++
# -w suppresses all warnings (the part that's commented out helps me find memory leaks, it ruins performance though!)
CFLAGS=-O3 #-g3 -fsanitize=address
CXXFLAGS=-Wall -Wno-unknown-pragmas -std=c++17 -O2 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) #-g3 -fsanitize=address
LDFLAGS=-lpthread -ldl #-g3 -fsanitize=address
# If compiling with ASAN, invoke like this: $ LSAN_OPTIONS=suppressions=suppr.txt bin/fusion
CFLAGS=-Wall -Wno-unknown-pragmas -O2 -fPIE -D_FORTIFY_SOURCE=1 -fstack-protector #-g3 -fsanitize=address
CXXFLAGS=$(CFLAGS) -std=c++17 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor
LDFLAGS=-lpthread -lsqlite3 -pie -Wl,-z,relro -Wl,-z,now #-g3 -fsanitize=address
# specifies the name of our exectuable
SERVER=bin/fusion
@@ -14,71 +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=-Wall -Wno-unknown-pragmas -std=c++17 -O3 -fno-tree-dce -fno-inline-small-functions -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) #-g3 -fsanitize=address
WIN_LDFLAGS=-static -lws2_32 -lwsock32 #-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/sqlite/sqlite3.c\
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/CombatManager.cpp\
src/CNLoginServer.cpp\
src/CNProtocol.cpp\
src/CNShardServer.cpp\
src/CNShared.cpp\
src/CNStructs.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/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/Player.cpp\
src/PlayerManager.cpp\
src/PlayerMovement.cpp\
src/BuiltinCommands.cpp\
src/settings.cpp\
src/TransportManager.cpp\
src/Transport.cpp\
src/TableData.cpp\
src/Chunking.cpp\
src/Buddies.cpp\
src/Groups.cpp\
src/Racing.cpp\
src/Vendors.cpp\
src/Trading.cpp\
src/Rand.cpp\
# headers (for timestamp purposes)
CHDR=\
src/contrib/sqlite/sqlite3.h\
src/contrib/sqlite/sqlite_orm.h\
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/CombatManager.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/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/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)
@@ -99,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 $@ $<
@@ -108,19 +151,25 @@ 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
$(CXX) $(OBJ) $(LDFLAGS) -o $(SERVER)
# compatibility with how cmake injects GIT_VERSION
version.h:
touch version.h
src/main.o: version.h
.PHONY: all windows clean nuke
# 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)
rm -f src/*.o src/*/*.o $(SERVER) $(WIN_SERVER) version.h
# gets rid of all compiled objects, including the libraries
nuke:
rm -f $(OBJ) $(SERVER) $(WIN_SERVER)
rm -f $(OBJ) $(SERVER) $(WIN_SERVER) version.h

View File

@@ -1,30 +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 landwalker server for FusionFall. It currently supports versions `beta-20100104` and `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.0/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 `Server/winfusion.exe`
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.
You have two randomized characters available to you on the Character Selection screen, one boy, one girl.
You can also make your own character and play through the tutorial. The tutorial can be skipped by pressing the ~ key.
If you want, [compiled binaries (artifacts) for each new commit can be found on AppVeyor.](https://ci.appveyor.com/project/OpenFusionProject/openfusion)
If you want to run the latest development builds of the server, [compiled binaries (artifacts) for each functional commit can be found here.](http://cdn.dexlabs.systems/of-builds/)
For a more detailed overview of the game's architecture and how to configure it, read the following sections.
@@ -43,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.
@@ -51,32 +64,27 @@ 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. 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.
### Makefile
@@ -85,21 +93,21 @@ 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
If you'd like to contribute to this project, please read [CONTRIBUTING.md](CONTRIBUTING.md).
## "Gameplay"
## Gameplay
Notice the quotes. This is not a full-fledged game that can be played.
It's what's called a landwalker; enough of the server has been implemented to allow players to run around in the game world, and not much else.
The goal of the project is to faithfully recreate the game as it was at the time of the targeted build.
The server is not yet complete, however, and some functionality is still missing.
Because the server is still in development, ordinary players are allowed access to a few admin commands:
![](res/sane_upsell.png)
To make your landwalking experience more pleasant, you can make use of a few admin commands to get around easier:
### Movement commands
* A `/speed` of around 2400 or 3000 is nice.
* A `/jump` of about 50 will send you soaring
@@ -116,6 +124,4 @@ To make your landwalking experience more pleasant, you can make use of a few adm
* `/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. Characters currently save only upon creation, any items add/traded will not be saved.
### A full list of commands can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).

View File

@@ -1,79 +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"
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"
}
artifacts:
- path: bin
name: ubuntu20_04-bin-x64
type: zip
-
matrix:
only:
- image: Visual Studio 2019
build_script:
- ps: |
$versions = "104", "728"
$configurations = "Release", "Debug"
# 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"
}
}
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,36 +10,92 @@
# 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=8001
# enables two randomly generated characters in the
# character selection menu for convenience
randomcharacters=true
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
# Shard Server configuration
[shard]
port=8002
port=23001
ip=127.0.0.1
# distance at which other players and NPCs become visible
playerdistance=20000
npcdistance=16000
# distance at which other players and NPCs become visible.
# this value is used for calculating chunk size
viewdistance=16000
# time, in milliseconds, to wait before kicking a non-responsive client
# default is 1 minute
timeout=60000
# should mobs move around and fight back?
# can be disabled for easier mob placement
simulatemobs=true
# little message players see when they enter the game
motd=Welcome to OpenFusion!
# NPC json data
npcdata=data/NPCs.json
# warp target json data
warpdata=data/warps.json
# mob json
mobdata=data/mobs.json
# is everyone a GM?
gm=true
# spawn coordinates (Z is height)
# the supplied defaults are at Sector V (future)
spawnx=632032
spawny=187177
spawnz=-5500
# The following are the default locations of the JSON files the server
# requires to run. You can override them by changing their values and
# uncommenting them (removing the leading # character from that line).
# location of the tabledata folder
#tdatadir=tdata/
# location of the patch folder
#patchdir=tdata/patch/
# 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
# should tutorial flags be disabled off the bat?
disablefirstuseflag=true
# account permission level that will be set upon character creation
# 1 = default, will allow *all* commands
# 30 = allow some more "abusable" commands such as /summon
# 50 = only allow cheat commands, like /itemN and /speed
# 99 = standard user account, no cheats allowed
# any number higher than 50 will disable commands
accountlevel=1
# should mobs drop event crates?
# 0 = no event
# 1 = Knishmas
# 2 = Halloween
# 3 = Easter
eventmode=0
# you can override the default spawn point.
# these example coords are for the Future (Z is height):
#spawnx=632032
#spawny=187177
#spawnz=-5500
# Player location monitor interface configuration
[monitor]
enabled=false
# the port to listen for connections on
port=8003
# how often the listeners should be updated (in milliseconds)
interval=5000

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
res/openfusion-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

18
sql/migration1.sql Normal file
View File

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

37
sql/migration2.sql Normal file
View File

@@ -0,0 +1,37 @@
/*
It is recommended in the SQLite manual to turn off
foreign keys when making schema changes that involve them
*/
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
-- New table to store code items
CREATE TABLE RedeemedCodes(
PlayerID INTEGER NOT NULL,
Code TEXT NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Code)
);
-- Change Coordinates in Players table to non-plural form
ALTER TABLE Players RENAME COLUMN XCoordinates TO XCoordinate;
ALTER TABLE Players RENAME COLUMN YCoordinates TO YCoordinate;
ALTER TABLE Players RENAME COLUMN ZCoordinates TO ZCoordinate;
-- Fix email attachments not being unique enough
CREATE TABLE Temp (
PlayerID INTEGER NOT NULL,
MsgIndex INTEGER NOT NULL,
Slot INTEGER NOT NULL,
ID INTEGER NOT NULL,
Type INTEGER NOT NULL,
Opt INTEGER NOT NULL,
TimeLimit INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, MsgIndex, Slot)
);
INSERT INTO Temp SELECT * FROM EmailItems;
DROP TABLE EmailItems;
ALTER TABLE Temp RENAME TO EmailItems;
-- Update DB Version
UPDATE Meta SET Value = 3 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;
PRAGMA foreign_keys=ON;

28
sql/migration3.sql Normal file
View File

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

161
sql/tables.sql Normal file
View File

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

804
src/Abilities.cpp Normal file
View File

@@ -0,0 +1,804 @@
#include "Abilities.hpp"
#include "PlayerManager.hpp"
#include "Player.hpp"
#include "NPCManager.hpp"
#include "Nanos.hpp"
#include "Groups.hpp"
#include "Eggs.hpp"
/*
* TODO: This file is in desperate need of deduplication and rewriting.
*/
std::map<int32_t, SkillData> Nanos::SkillTable;
/*
* targetData approach
* first integer is the count
* second to fifth integers are IDs, these can be either player iID or mob's iID
*/
std::vector<int> Nanos::findTargets(Player* plr, int skillID, CNPacketData* data) {
std::vector<int> tD(5);
if (SkillTable[skillID].targetType <= 2 && data != nullptr) { // client gives us the targets
sP_CL2FE_REQ_NANO_SKILL_USE* pkt = (sP_CL2FE_REQ_NANO_SKILL_USE*)data->buf;
// validate request check
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_NANO_SKILL_USE), pkt->iTargetCnt, sizeof(int32_t), data->size)) {
std::cout << "[WARN] bad sP_CL2FE_REQ_NANO_SKILL_USE packet size" << std::endl;
return tD;
}
int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_NANO_SKILL_USE));
tD[0] = pkt->iTargetCnt;
for (int i = 0; i < pkt->iTargetCnt; i++)
tD[i+1] = pktdata[i];
} else if (SkillTable[skillID].targetType == 2) { // self target only
tD[0] = 1;
tD[1] = plr->iID;
} else if (SkillTable[skillID].targetType == 3) { // entire group as target
Player *otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return tD;
if (SkillTable[skillID].effectArea == 0) { // for buffs
tD[0] = otherPlr->groupCnt;
for (int i = 0; i < otherPlr->groupCnt; i++)
tD[i+1] = otherPlr->groupIDs[i];
return tD;
}
for (int i = 0; i < otherPlr->groupCnt; i++) { // group heals have an area limit
Player *otherPlr2 = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
if (otherPlr2 == nullptr)
continue;
if (true) {//hypot(otherPlr2->x - plr->x, otherPlr2->y - plr->y) < SkillTable[skillID].effectArea) {
tD[i+1] = otherPlr->groupIDs[i];
tD[0] += 1;
}
}
}
return tD;
}
void Nanos::nanoUnbuff(CNSocket* sock, std::vector<int> targetData, int32_t bitFlag, int16_t timeBuffID, int16_t amount, bool groupPower) {
Player *plr = PlayerManager::getPlayer(sock);
plr->iSelfConditionBitFlag &= ~bitFlag;
int groupFlags = 0;
if (groupPower) {
plr->iGroupConditionBitFlag &= ~bitFlag;
Player *leader = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (leader != nullptr)
groupFlags = Groups::getGroupFlags(leader);
}
for (int i = 0; i < targetData[0]; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(targetData[i+1]);
if (!((groupFlags | varPlr->iSelfConditionBitFlag) & bitFlag)) {
CNSocket* sockTo = PlayerManager::getSockFromID(targetData[i+1]);
if (sockTo == nullptr)
continue; // sanity check
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp);
resp.eCSTB = timeBuffID; // eCharStatusTimeBuffID
resp.eTBU = 2; // eTimeBuffUpdate
resp.eTBT = 1; // eTimeBuffType 1 means nano
varPlr->iConditionBitFlag &= ~bitFlag;
resp.iConditionBitFlag = varPlr->iConditionBitFlag |= groupFlags | varPlr->iSelfConditionBitFlag;
if (amount > 0)
resp.TimeBuff.iValue = amount;
sockTo->sendPacket((void*)&resp, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
}
}
int Nanos::applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags) {
if (SkillTable[skillID].drainType == 1)
return 0;
int32_t bitFlag = 0;
for (auto& pwr : NanoPowers) {
if (pwr.skillType == SkillTable[skillID].skillType) {
bitFlag = pwr.bitFlag;
Player *plr = PlayerManager::getPlayer(sock);
if (eTBU == 1 || !((groupFlags | plr->iSelfConditionBitFlag) & bitFlag)) {
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp);
resp.eCSTB = pwr.timeBuffID;
resp.eTBU = eTBU;
resp.eTBT = eTBT;
if (eTBU == 1)
plr->iConditionBitFlag |= bitFlag;
else
plr->iConditionBitFlag &= ~bitFlag;
resp.iConditionBitFlag = plr->iConditionBitFlag |= groupFlags | plr->iSelfConditionBitFlag;
resp.TimeBuff.iValue = SkillTable[skillID].powerIntensity[0];
sock->sendPacket((void*)&resp, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
return bitFlag;
}
}
return 0;
}
#pragma region Nano Powers
namespace Nanos {
bool doDebuff(CNSocket *sock, sSkillResult_Buff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) {
std::cout << "[WARN] doDebuff: NPC ID not found" << std::endl;
return false;
}
BaseNPC* npc = NPCManager::NPCs[targetID];
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] doDebuff: NPC is not a mob" << std::endl;
return false;
}
Mob* mob = (Mob*)npc;
Combat::hitMob(sock, mob, 0);
respdata[i].eCT = 4;
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].bProtected = 1;
if (mob->skillStyle < 0 && mob->state != MobState::RETREAT
&& !(mob->appearanceData.iConditionBitFlag & CSB_BIT_FREEDOM)) { // only debuff if the enemy is not retreating, casting corruption or in freedom
mob->appearanceData.iConditionBitFlag |= bitFlag;
mob->unbuffTimes[bitFlag] = getTime() + duration * 100;
respdata[i].bProtected = 0;
}
respdata[i].iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
return true;
}
bool doBuff(CNSocket *sock, sSkillResult_Buff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
Player *plr = nullptr;
CNSocket *sockTo = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == targetID) {
sockTo = pair.first;
plr = pair.second;
break;
}
}
// player not found
if (sockTo == nullptr || plr == nullptr) {
std::cout << "[WARN] doBuff: player ID not found" << std::endl;
return false;
}
respdata[i].eCT = 1;
respdata[i].iID = plr->iID;
respdata[i].iConditionBitFlag = 0;
// only apply buffs if the player is actually alive
if (plr->HP > 0) {
respdata[i].iConditionBitFlag = bitFlag;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = timeBuffID; // eCharStatusTimeBuffID
pkt.eTBU = 1; // eTimeBuffUpdate
pkt.eTBT = 1; // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = plr->iConditionBitFlag |= bitFlag;
if (amount > 0)
pkt.TimeBuff.iValue = amount;
sockTo->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
return true;
}
bool doDamageNDebuff(CNSocket *sock, sSkillResult_Damage_N_Debuff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) {
// not sure how to best handle this
std::cout << "[WARN] doDamageNDebuff: NPC ID not found" << std::endl;
return false;
}
BaseNPC* npc = NPCManager::NPCs[targetID];
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] doDamageNDebuff: NPC is not a mob" << std::endl;
return false;
}
Mob* mob = (Mob*)npc;
Combat::hitMob(sock, mob, 0); // just to gain aggro
respdata[i].eCT = 4;
respdata[i].iDamage = duration / 10;
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].bProtected = 1;
if (mob->skillStyle < 0 && mob->state != MobState::RETREAT
&& !(mob->appearanceData.iConditionBitFlag & CSB_BIT_FREEDOM)) { // only debuff if the enemy is not retreating, casting corruption or in freedom
mob->appearanceData.iConditionBitFlag |= bitFlag;
mob->unbuffTimes[bitFlag] = getTime() + duration * 100;
respdata[i].bProtected = 0;
}
respdata[i].iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
return true;
}
bool doHeal(CNSocket *sock, sSkillResult_Heal_HP *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
Player *plr = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == targetID) {
plr = pair.second;
break;
}
}
// player not found
if (plr == nullptr) {
std::cout << "[WARN] doHeal: player ID not found" << std::endl;
return false;
}
int healedAmount = PC_MAXHEALTH(plr->level) * amount / 1000;
// do not heal dead players
if (plr->HP <= 0)
healedAmount = 0;
plr->HP += healedAmount;
if (plr->HP > PC_MAXHEALTH(plr->level))
plr->HP = PC_MAXHEALTH(plr->level);
respdata[i].eCT = 1;
respdata[i].iID = plr->iID;
respdata[i].iHP = plr->HP;
respdata[i].iHealHP = healedAmount;
return true;
}
bool doDamage(CNSocket *sock, sSkillResult_Damage *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) {
// not sure how to best handle this
std::cout << "[WARN] doDamage: NPC ID not found" << std::endl;
return false;
}
BaseNPC* npc = NPCManager::NPCs[targetID];
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] doDamage: NPC is not a mob" << std::endl;
return false;
}
Mob* mob = (Mob*)npc;
Player *plr = PlayerManager::getPlayer(sock);
int damage = Combat::hitMob(sock, mob, std::max(PC_MAXHEALTH(plr->level) * amount / 1000, mob->maxHealth * amount / 1000));
respdata[i].eCT = 4;
respdata[i].iDamage = damage;
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iHP = mob->appearanceData.iHP;
return true;
}
/*
* NOTE: Leech is specially encoded.
*
* It manages to fit inside the nanoPower<>() mold with only a slight hack,
* but it really is it's own thing. There is a hard assumption that players
* will only every leech a single mob, and the sanity check that enforces that
* assumption is critical.
*/
bool doLeech(CNSocket *sock, sSkillResult_Heal_HP *healdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
// this sanity check is VERY important
if (i != 0) {
std::cout << "[WARN] Player attempted to leech more than one mob!" << std::endl;
return false;
}
sSkillResult_Damage *damagedata = (sSkillResult_Damage*)(((uint8_t*)healdata) + sizeof(sSkillResult_Heal_HP));
Player *plr = PlayerManager::getPlayer(sock);
int healedAmount = amount;
plr->HP += healedAmount;
if (plr->HP > PC_MAXHEALTH(plr->level))
plr->HP = PC_MAXHEALTH(plr->level);
healdata->eCT = 1;
healdata->iID = plr->iID;
healdata->iHP = plr->HP;
healdata->iHealHP = healedAmount;
if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) {
// not sure how to best handle this
std::cout << "[WARN] doLeech: NPC ID not found" << std::endl;
return false;
}
BaseNPC* npc = NPCManager::NPCs[targetID];
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] doLeech: NPC is not a mob" << std::endl;
return false;
}
Mob* mob = (Mob*)npc;
int damage = Combat::hitMob(sock, mob, amount * 2);
damagedata->eCT = 4;
damagedata->iDamage = damage;
damagedata->iID = mob->appearanceData.iNPC_ID;
damagedata->iHP = mob->appearanceData.iHP;
return true;
}
bool doResurrect(CNSocket *sock, sSkillResult_Resurrect *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
Player *plr = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == targetID) {
plr = pair.second;
break;
}
}
// player not found
if (plr == nullptr) {
std::cout << "[WARN] doResurrect: player ID not found" << std::endl;
return false;
}
respdata[i].eCT = 1;
respdata[i].iID = plr->iID;
respdata[i].iRegenHP = plr->HP;
return true;
}
bool doMove(CNSocket *sock, sSkillResult_Move *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
Player *plr = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == targetID) {
plr = pair.second;
break;
}
}
// player not found
if (plr == nullptr) {
std::cout << "[WARN] doMove: player ID not found" << std::endl;
return false;
}
Player *plr2 = PlayerManager::getPlayer(sock);
respdata[i].eCT = 1;
respdata[i].iID = plr->iID;
respdata[i].iMapNum = plr2->recallInstance;
respdata[i].iMoveX = plr2->recallX;
respdata[i].iMoveY = plr2->recallY;
respdata[i].iMoveZ = plr2->recallZ;
return true;
}
template<class sPAYLOAD,
bool (*work)(CNSocket*,sPAYLOAD*,int,int32_t,int32_t,int16_t,int16_t,int16_t)>
void nanoPower(CNSocket *sock, std::vector<int> targetData,
int16_t nanoID, int16_t skillID, int16_t duration, int16_t amount,
int16_t skillType, int32_t bitFlag, int16_t timeBuffID) {
Player *plr = PlayerManager::getPlayer(sock);
if (skillType == EST_RETROROCKET_SELF || skillType == EST_RECALL) // rocket and self recall does not need any trailing structs
targetData[0] = 0;
size_t resplen;
// special case since leech is atypically encoded
if (skillType == EST_BLOODSUCKING)
resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) + sizeof(sSkillResult_Heal_HP) + sizeof(sSkillResult_Damage);
else
resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) + targetData[0] * sizeof(sPAYLOAD);
// validate response packet
if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), targetData[0], sizeof(sPAYLOAD))) {
std::cout << "[WARN] bad sP_FE2CL_NANO_SKILL_USE packet size" << std::endl;
return;
}
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_NANO_SKILL_USE_SUCC *resp = (sP_FE2CL_NANO_SKILL_USE_SUCC*)respbuf;
sPAYLOAD *respdata = (sPAYLOAD*)(respbuf+sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC));
resp->iPC_ID = plr->iID;
resp->iSkillID = skillID;
resp->iNanoID = nanoID;
resp->iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
resp->eST = skillType;
resp->iTargetCnt = targetData[0];
if (SkillTable[skillID].drainType == 2) {
if (SkillTable[skillID].targetType >= 2)
plr->iSelfConditionBitFlag |= bitFlag;
if (SkillTable[skillID].targetType == 3)
plr->iGroupConditionBitFlag |= bitFlag;
}
for (int i = 0; i < targetData[0]; i++)
if (!work(sock, respdata, i, targetData[i+1], bitFlag, timeBuffID, duration, amount))
return;
sock->sendPacket((void*)&respbuf, P_FE2CL_NANO_SKILL_USE_SUCC, resplen);
assert(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) == sizeof(sP_FE2CL_NANO_SKILL_USE));
if (skillType == EST_RECALL_GROUP) { // in the case of group recall, nobody but group members need the packet
for (int i = 0; i < targetData[0]; i++) {
CNSocket *sock2 = PlayerManager::getSockFromID(targetData[i+1]);
sock2->sendPacket((void*)&respbuf, P_FE2CL_NANO_SKILL_USE, resplen);
}
} else
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NANO_SKILL_USE, resplen);
// Warping on recall
if (skillType == EST_RECALL || skillType == EST_RECALL_GROUP) {
if ((int32_t)plr->instanceID == plr->recallInstance)
PlayerManager::sendPlayerTo(sock, plr->recallX, plr->recallY, plr->recallZ, plr->recallInstance);
else {
INITSTRUCT(sP_FE2CL_REP_WARP_USE_RECALL_FAIL, response)
sock->sendPacket((void*)&response, P_FE2CL_REP_WARP_USE_RECALL_FAIL, sizeof(sP_FE2CL_REP_WARP_USE_RECALL_FAIL));
}
}
}
// nano power dispatch table
std::vector<NanoPower> NanoPowers = {
NanoPower(EST_STUN, CSB_BIT_STUN, ECSB_STUN, nanoPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
NanoPower(EST_HEAL_HP, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Heal_HP, doHeal>),
NanoPower(EST_BOUNDINGBALL, CSB_BIT_BOUNDINGBALL, ECSB_BOUNDINGBALL, nanoPower<sSkillResult_Buff, doDebuff>),
NanoPower(EST_SNARE, CSB_BIT_DN_MOVE_SPEED, ECSB_DN_MOVE_SPEED, nanoPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
NanoPower(EST_DAMAGE, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Damage, doDamage>),
NanoPower(EST_BLOODSUCKING, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Heal_HP, doLeech>),
NanoPower(EST_SLEEP, CSB_BIT_MEZ, ECSB_MEZ, nanoPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
NanoPower(EST_REWARDBLOB, CSB_BIT_REWARD_BLOB, ECSB_REWARD_BLOB, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_RUN, CSB_BIT_UP_MOVE_SPEED, ECSB_UP_MOVE_SPEED, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_REWARDCASH, CSB_BIT_REWARD_CASH, ECSB_REWARD_CASH, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_PROTECTBATTERY, CSB_BIT_PROTECT_BATTERY, ECSB_PROTECT_BATTERY, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_MINIMAPENEMY, CSB_BIT_MINIMAP_ENEMY, ECSB_MINIMAP_ENEMY, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_PROTECTINFECTION, CSB_BIT_PROTECT_INFECTION, ECSB_PROTECT_INFECTION, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_JUMP, CSB_BIT_UP_JUMP_HEIGHT, ECSB_UP_JUMP_HEIGHT, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_FREEDOM, CSB_BIT_FREEDOM, ECSB_FREEDOM, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_PHOENIX, CSB_BIT_PHOENIX, ECSB_PHOENIX, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_STEALTH, CSB_BIT_UP_STEALTH, ECSB_UP_STEALTH, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_MINIMAPTRESURE, CSB_BIT_MINIMAP_TRESURE, ECSB_MINIMAP_TRESURE, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_RECALL, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Move, doMove>),
NanoPower(EST_RECALL_GROUP, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Move, doMove>),
NanoPower(EST_RETROROCKET_SELF, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_PHOENIX_GROUP, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Resurrect, doResurrect>),
NanoPower(EST_NANOSTIMPAK, CSB_BIT_STIMPAKSLOT1, ECSB_STIMPAKSLOT1, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_NANOSTIMPAK, CSB_BIT_STIMPAKSLOT2, ECSB_STIMPAKSLOT2, nanoPower<sSkillResult_Buff, doBuff>),
NanoPower(EST_NANOSTIMPAK, CSB_BIT_STIMPAKSLOT3, ECSB_STIMPAKSLOT3, nanoPower<sSkillResult_Buff, doBuff>)
};
}; // namespace
#pragma endregion
#pragma region Mob Powers
namespace Combat {
bool doDamageNDebuff(Mob *mob, sSkillResult_Damage_N_Debuff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
CNSocket *sock = nullptr;
Player *plr = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == targetID) {
sock = pair.first;
plr = pair.second;
break;
}
}
// player not found
if (plr == nullptr) {
std::cout << "[WARN] doDamageNDebuff: player ID not found" << std::endl;
return false;
}
respdata[i].eCT = 1;
respdata[i].iDamage = duration / 10;
respdata[i].iID = plr->iID;
respdata[i].iHP = plr->HP;
respdata[i].iStamina = plr->Nanos[plr->activeNano].iStamina;
if (plr->iConditionBitFlag & CSB_BIT_FREEDOM)
respdata[i].bProtected = 1;
else {
if (!(plr->iConditionBitFlag & bitFlag)) {
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = timeBuffID; // eCharStatusTimeBuffID
pkt.eTBU = 1; // eTimeBuffUpdate
pkt.eTBT = 2;
pkt.iConditionBitFlag = plr->iConditionBitFlag |= bitFlag;
pkt.TimeBuff.iValue = amount * 5;
sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
respdata[i].bProtected = 0;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, bitFlag);
time_t until = getTime() + (time_t)duration * 100;
Eggs::EggBuffs[key] = until;
}
respdata[i].iConditionBitFlag = plr->iConditionBitFlag;
if (plr->HP <= 0) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!MobAI::aggroCheck(mob, getTime())) {
MobAI::clearDebuff(mob);
if (mob->groupLeader != 0)
MobAI::groupRetreat(mob);
}
}
return true;
}
bool doHeal(Mob *mob, sSkillResult_Heal_HP *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) {
std::cout << "[WARN] doHeal: NPC ID not found" << std::endl;
return false;
}
BaseNPC* npc = NPCManager::NPCs[targetID];
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] doHeal: NPC is not a mob" << std::endl;
return false;
}
Mob* targetMob = (Mob*)npc;
int healedAmount = amount * targetMob->maxHealth / 1000;
targetMob->appearanceData.iHP += healedAmount;
if (targetMob->appearanceData.iHP > targetMob->maxHealth)
targetMob->appearanceData.iHP = targetMob->maxHealth;
respdata[i].eCT = 4;
respdata[i].iID = targetMob->appearanceData.iNPC_ID;
respdata[i].iHP = targetMob->appearanceData.iHP;
respdata[i].iHealHP = healedAmount;
return true;
}
bool doReturnHeal(Mob *mob, sSkillResult_Heal_HP *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
int healedAmount = amount * mob->maxHealth / 1000;
mob->appearanceData.iHP += healedAmount;
if (mob->appearanceData.iHP > mob->maxHealth)
mob->appearanceData.iHP = mob->maxHealth;
respdata[i].eCT = 4;
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHealHP = healedAmount;
return true;
}
bool doDamage(Mob *mob, sSkillResult_Damage *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
Player *plr = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == targetID) {
plr = pair.second;
break;
}
}
// player not found
if (plr == nullptr) {
std::cout << "[WARN] doDamage: player ID not found" << std::endl;
return false;
}
int damage = amount * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)
damage = 0;
respdata[i].eCT = 1;
respdata[i].iDamage = damage;
respdata[i].iID = plr->iID;
respdata[i].iHP = plr->HP -= damage;
if (plr->HP <= 0) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!MobAI::aggroCheck(mob, getTime())) {
MobAI::clearDebuff(mob);
if (mob->groupLeader != 0)
MobAI::groupRetreat(mob);
}
}
return true;
}
bool doLeech(Mob *mob, sSkillResult_Heal_HP *healdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
// this sanity check is VERY important
if (i != 0) {
std::cout << "[WARN] Mob attempted to leech more than one player!" << std::endl;
return false;
}
Player *plr = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == targetID) {
plr = pair.second;
break;
}
}
// player not found
if (plr == nullptr) {
std::cout << "[WARN] doLeech: player ID not found" << std::endl;
return false;
}
sSkillResult_Damage *damagedata = (sSkillResult_Damage*)(((uint8_t*)healdata) + sizeof(sSkillResult_Heal_HP));
int healedAmount = amount * PC_MAXHEALTH(plr->level) / 1000;
mob->appearanceData.iHP += healedAmount;
if (mob->appearanceData.iHP > mob->maxHealth)
mob->appearanceData.iHP = mob->maxHealth;
healdata->eCT = 4;
healdata->iID = mob->appearanceData.iNPC_ID;
healdata->iHP = mob->appearanceData.iHP;
healdata->iHealHP = healedAmount;
int damage = healedAmount;
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)
damage = 0;
damagedata->eCT = 1;
damagedata->iDamage = damage;
damagedata->iID = plr->iID;
damagedata->iHP = plr->HP -= damage;
if (plr->HP <= 0) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!MobAI::aggroCheck(mob, getTime())) {
MobAI::clearDebuff(mob);
if (mob->groupLeader != 0)
MobAI::groupRetreat(mob);
}
}
return true;
}
bool doBatteryDrain(Mob *mob, sSkillResult_BatteryDrain *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
Player *plr = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == targetID) {
plr = pair.second;
break;
}
}
// player not found
if (plr == nullptr) {
std::cout << "[WARN] doBatteryDrain: player ID not found" << std::endl;
return false;
}
respdata[i].eCT = 1;
respdata[i].iID = plr->iID;
if (plr->iConditionBitFlag & CSB_BIT_PROTECT_BATTERY) {
respdata[i].bProtected = 1;
respdata[i].iDrainW = 0;
respdata[i].iDrainN = 0;
} else {
respdata[i].bProtected = 0;
respdata[i].iDrainW = amount * (18 + (int)mob->data["m_iNpcLevel"]) / 36;
respdata[i].iDrainN = amount * (18 + (int)mob->data["m_iNpcLevel"]) / 36;
}
respdata[i].iBatteryW = plr->batteryW -= (respdata[i].iDrainW < plr->batteryW) ? respdata[i].iDrainW : plr->batteryW;
respdata[i].iBatteryN = plr->batteryN -= (respdata[i].iDrainN < plr->batteryN) ? respdata[i].iDrainN : plr->batteryN;
respdata[i].iStamina = plr->Nanos[plr->activeNano].iStamina;
respdata[i].iConditionBitFlag = plr->iConditionBitFlag;
return true;
}
bool doBuff(Mob *mob, sSkillResult_Buff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
respdata[i].eCT = 4;
respdata[i].iID = mob->appearanceData.iNPC_ID;
mob->appearanceData.iConditionBitFlag |= bitFlag;
respdata[i].iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
return true;
}
template<class sPAYLOAD,
bool (*work)(Mob*,sPAYLOAD*,int,int32_t,int32_t,int16_t,int16_t,int16_t)>
void mobPower(Mob *mob, std::vector<int> targetData,
int16_t skillID, int16_t duration, int16_t amount,
int16_t skillType, int32_t bitFlag, int16_t timeBuffID) {
size_t resplen;
// special case since leech is atypically encoded
if (skillType == EST_BLOODSUCKING)
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Heal_HP) + sizeof(sSkillResult_Damage);
else
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + targetData[0] * sizeof(sPAYLOAD);
// validate response packet
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), targetData[0], sizeof(sPAYLOAD))) {
std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_HIT packet size" << std::endl;
return;
}
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_NPC_SKILL_HIT *resp = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
sPAYLOAD *respdata = (sPAYLOAD*)(respbuf+sizeof(sP_FE2CL_NPC_SKILL_HIT));
resp->iNPC_ID = mob->appearanceData.iNPC_ID;
resp->iSkillID = skillID;
resp->iValue1 = mob->hitX;
resp->iValue2 = mob->hitY;
resp->iValue3 = mob->hitZ;
resp->eST = skillType;
resp->iTargetCnt = targetData[0];
for (int i = 0; i < targetData[0]; i++)
if (!work(mob, respdata, i, targetData[i+1], bitFlag, timeBuffID, duration, amount))
return;
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
}
// nano power dispatch table
std::vector<MobPower> MobPowers = {
MobPower(EST_STUN, CSB_BIT_STUN, ECSB_STUN, mobPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
MobPower(EST_HEAL_HP, CSB_BIT_NONE, ECSB_NONE, mobPower<sSkillResult_Heal_HP, doHeal>),
MobPower(EST_RETURNHOMEHEAL, CSB_BIT_NONE, ECSB_NONE, mobPower<sSkillResult_Heal_HP, doReturnHeal>),
MobPower(EST_SNARE, CSB_BIT_DN_MOVE_SPEED, ECSB_DN_MOVE_SPEED, mobPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
MobPower(EST_DAMAGE, CSB_BIT_NONE, ECSB_NONE, mobPower<sSkillResult_Damage, doDamage>),
MobPower(EST_BATTERYDRAIN, CSB_BIT_NONE, ECSB_NONE, mobPower<sSkillResult_BatteryDrain, doBatteryDrain>),
MobPower(EST_SLEEP, CSB_BIT_MEZ, ECSB_MEZ, mobPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
MobPower(EST_BLOODSUCKING, CSB_BIT_NONE, ECSB_NONE, mobPower<sSkillResult_Heal_HP, doLeech>),
MobPower(EST_FREEDOM, CSB_BIT_FREEDOM, ECSB_FREEDOM, mobPower<sSkillResult_Buff, doBuff>)
};
}; // namespace
#pragma endregion

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

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,412 +0,0 @@
#include "CNLoginServer.hpp"
#include "CNShared.hpp"
#include "CNStructs.hpp"
#include "Database.hpp"
#include "PlayerManager.hpp"
#include <regex>
#include "contrib/bcrypt/BCrypt.hpp"
#include "settings.hpp"
std::map<CNSocket*, CNLoginData> CNLoginServer::loginSessions;
CNLoginServer::CNLoginServer(uint16_t p) {
port = p;
pHandler = &CNLoginServer::handlePacket;
init();
}
void CNLoginServer::handlePacket(CNSocket* sock, CNPacketData* data) {
printPacket(data, CL2LS);
switch (data->type) {
case P_CL2LS_REQ_LOGIN: {
if (data->size != sizeof(sP_CL2LS_REQ_LOGIN))
return; // ignore the malformed packet
sP_CL2LS_REQ_LOGIN* login = (sP_CL2LS_REQ_LOGIN*)data->buf;
//TODO: implement better way of sending credentials
std::string userLogin = U16toU8(login->szID);
std::string userPassword = U16toU8(login->szPassword);
bool success = false;
int errorCode = 0;
//checking regex
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword))
{
errorCode = (int)LOGINERRORID::login_error;
}
else
{
std::unique_ptr<Database::Account> findUser = Database::findAccount(userLogin);
//if account not found, create it
if (findUser == nullptr)
{
loginSessions[sock] = CNLoginData();
loginSessions[sock].userID = Database::addAccount(userLogin, userPassword);
loginSessions[sock].slot = 1;
success = true;
}
//if user exists, check if password is correct
else if (CNLoginServer::isPasswordCorrect(findUser->Password, userPassword))
{
//check if account isn't currently in use
if (CNLoginServer::isAccountInUse(findUser->AccountID) ||
PlayerManager::isAccountInUse(findUser->AccountID))
{
errorCode = (int)LOGINERRORID::id_already_in_use;
}
//if not, login success
else
{
loginSessions[sock] = CNLoginData();
loginSessions[sock].userID = findUser->AccountID;
loginSessions[sock].slot = findUser->Selected;
success = true;
}
}
else
{
errorCode = (int)LOGINERRORID::id_and_password_do_not_match;
}
}
if (success)
{
std::vector<Player> characters = Database::getCharacters(loginSessions[sock].userID);
int charCount = characters.size();
INITSTRUCT(sP_LS2CL_REP_LOGIN_SUCC, resp);
// set username in resp packet
memcpy(resp.szID, login->szID, sizeof(char16_t) * 33);
resp.iCharCount = charCount;
resp.iSlotNum = loginSessions[sock].slot;
resp.iPaymentFlag = 1;
resp.iOpenBetaFlag = 0;
resp.uiSvrTime = getTime();
// send the resp in with original key
sock->sendPacket((void*)&resp, P_LS2CL_REP_LOGIN_SUCC, sizeof(sP_LS2CL_REP_LOGIN_SUCC));
// update keys
sock->setEKey(CNSocketEncryption::createNewKey(resp.uiSvrTime, resp.iCharCount + 1, resp.iSlotNum + 1));
sock->setFEKey(CNSocketEncryption::createNewKey((uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]), login->iClientVerC, 1));
// now send the characters :)
std::vector<Player>::iterator it;
for (it = characters.begin(); it != characters.end(); it++)
{
sP_LS2CL_REP_CHAR_INFO charInfo = sP_LS2CL_REP_CHAR_INFO();
charInfo.iSlot = (int8_t)it->slot;
charInfo.iLevel = (int16_t)it->level;
charInfo.sPC_Style = it->PCStyle;
charInfo.sPC_Style2 = it->PCStyle2;
// position
charInfo.iX = it->x;
charInfo.iY = it->y;
charInfo.iZ = it->z;
//save character in session (for char select)
int UID = it->iID;
loginSessions[sock].characters[UID] = Player(*it);
loginSessions[sock].characters[UID].FEKey = sock->getFEKey();
//temporary inventory stuff
for (int i = 0; i < 4; i++) {
//equip char creation clothes and lightning rifle
charInfo.aEquip[i] = it->Equip[i];
}
for (int i = 5; i < AEQUIP_COUNT; i++) {
// empty equips
charInfo.aEquip[i].iID = 0;
charInfo.aEquip[i].iType = i;
charInfo.aEquip[i].iOpt = 0;
}
// set default to the first character
if (it == characters.begin())
loginSessions[sock].selectedChar = UID;
sock->sendPacket((void*)&charInfo, P_LS2CL_REP_CHAR_INFO, sizeof(sP_LS2CL_REP_CHAR_INFO));
}
}
//Failure
else {
INITSTRUCT(sP_LS2CL_REP_LOGIN_FAIL, resp);
memcpy(resp.szID, login->szID, sizeof(char16_t) * 33);
resp.iErrorCode = errorCode;
sock->sendPacket((void*)&resp, P_LS2CL_REP_LOGIN_FAIL, sizeof(sP_LS2CL_REP_LOGIN_FAIL));
}
break;
}
case P_CL2LS_REP_LIVE_CHECK: {
// stubbed, the client really doesn't care LOL
break;
}
case P_CL2LS_REQ_CHECK_CHAR_NAME: {
if (data->size != sizeof(sP_CL2LS_REQ_CHECK_CHAR_NAME))
return;
// naughty words allowed!!!!!!!! (also for some reason, the client will always show 'Player 0' if you manually type a name. It will show up for other connected players though)
sP_CL2LS_REQ_CHECK_CHAR_NAME* nameCheck = (sP_CL2LS_REQ_CHECK_CHAR_NAME*)data->buf;
//check if name is occupied
if (Database::isNameFree(nameCheck))
{
// naughty words allowed!!!!!!!! (also for some reason, the client will always show 'Player + ID' if you manually type a name. It will show up for other connected players though)
INITSTRUCT(sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC, resp);
DEBUGLOG(
std::cout << "P_CL2LS_REQ_CHECK_CHAR_NAME:" << std::endl;
std::cout << "\tFirstName: " << U16toU8(nameCheck->szFirstName) << " LastName: " << U16toU8(nameCheck->szLastName) << std::endl;
)
memcpy(resp.szFirstName, nameCheck->szFirstName, sizeof(char16_t) * 9);
memcpy(resp.szLastName, nameCheck->szLastName, sizeof(char16_t) * 17);
// fr*ck allowed!!!
sock->sendPacket((void*)&resp, P_LS2CL_REP_CHECK_CHAR_NAME_SUCC, sizeof(sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC));
}
else {
INITSTRUCT(sP_LS2CL_REP_CHECK_CHAR_NAME_FAIL, resp);
resp.iErrorCode = 1;
sock->sendPacket((void*)&resp, P_LS2CL_REP_CHECK_CHAR_NAME_FAIL, sizeof(sP_LS2CL_REP_CHECK_CHAR_NAME_FAIL));
}
break;
}
case P_CL2LS_REQ_SAVE_CHAR_NAME: {
if (data->size != sizeof(sP_CL2LS_REQ_SAVE_CHAR_NAME))
return;
sP_CL2LS_REQ_SAVE_CHAR_NAME* save = (sP_CL2LS_REQ_SAVE_CHAR_NAME*)data->buf;
INITSTRUCT(sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC, resp);
DEBUGLOG(
std::cout << "P_CL2LS_REQ_SAVE_CHAR_NAME:" << std::endl;
std::cout << "\tSlot: " << (int)save->iSlotNum << std::endl;
std::cout << "\tName: " << U16toU8(save->szFirstName) << " " << U16toU8(save->szLastName) << std::endl;
)
resp.iSlotNum = save->iSlotNum;
resp.iGender = save->iGender;
resp.iPC_UID = Database::createCharacter(save, loginSessions[sock].userID);
memcpy(resp.szFirstName, save->szFirstName, sizeof(char16_t) * 9);
memcpy(resp.szLastName, save->szLastName, sizeof(char16_t) * 17);
sock->sendPacket((void*)&resp, P_LS2CL_REP_SAVE_CHAR_NAME_SUCC, sizeof(sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC));
Database::updateSelected(loginSessions[sock].userID, save->iSlotNum);
break;
}
case P_CL2LS_REQ_CHAR_CREATE: {
if (data->size != sizeof(sP_CL2LS_REQ_CHAR_CREATE))
return;
sP_CL2LS_REQ_CHAR_CREATE* character = (sP_CL2LS_REQ_CHAR_CREATE*)data->buf;
Database::finishCharacter(character);
DEBUGLOG(
std::cout << "P_CL2LS_REQ_CHAR_CREATE:" << std::endl;
std::cout << "\tPC_UID: " << character->PCStyle.iPC_UID << std::endl;
std::cout << "\tNameCheck: " << (int)character->PCStyle.iNameCheck << std::endl;
std::cout << "\tName: " << U16toU8(character->PCStyle.szFirstName) << " " << U16toU8(character->PCStyle.szLastName) << std::endl;
std::cout << "\tGender: " << (int)character->PCStyle.iGender << std::endl;
std::cout << "\tFace: " << (int)character->PCStyle.iFaceStyle << std::endl;
std::cout << "\tHair: " << (int)character->PCStyle.iHairStyle << std::endl;
std::cout << "\tHair Color: " << (int)character->PCStyle.iHairColor << std::endl;
std::cout << "\tSkin Color: " << (int)character->PCStyle.iSkinColor << std::endl;
std::cout << "\tEye Color: " << (int)character->PCStyle.iEyeColor << std::endl;
std::cout << "\tHeight: " << (int)character->PCStyle.iHeight << std::endl;
std::cout << "\tBody: " << (int)character->PCStyle.iBody << std::endl;
std::cout << "\tClass: " << (int)character->PCStyle.iClass << std::endl;
std::cout << "\tiEquipUBID: " << (int)character->sOn_Item.iEquipUBID << std::endl;
std::cout << "\tiEquipLBID: " << (int)character->sOn_Item.iEquipLBID << std::endl;
std::cout << "\tiEquipFootID: " << (int)character->sOn_Item.iEquipFootID << std::endl;
)
Player player =
Database::DbToPlayer(
Database::getDbPlayerById(character->PCStyle.iPC_UID)
);
int64_t UID = player.iID;
INITSTRUCT(sP_LS2CL_REP_CHAR_CREATE_SUCC, resp);
resp.sPC_Style = player.PCStyle;
resp.sPC_Style2 = player.PCStyle2;
resp.iLevel = player.level;
resp.sOn_Item = character->sOn_Item;
//save player in session
loginSessions[sock].characters[UID] = Player(player);
loginSessions[sock].characters[UID].FEKey = sock->getFEKey();
sock->sendPacket((void*)&resp, P_LS2CL_REP_CHAR_CREATE_SUCC, sizeof(sP_LS2CL_REP_CHAR_CREATE_SUCC));
Database::updateSelected(loginSessions[sock].userID, player.slot);
break;
}
case P_CL2LS_REQ_CHAR_DELETE: {
if (data->size != sizeof(sP_CL2LS_REQ_CHAR_DELETE))
return;
sP_CL2LS_REQ_CHAR_DELETE* del = (sP_CL2LS_REQ_CHAR_DELETE*)data->buf;
int operationResult = Database::deleteCharacter(del->iPC_UID);
INITSTRUCT(sP_LS2CL_REP_CHAR_DELETE_SUCC, resp);
resp.iSlotNum = operationResult;
sock->sendPacket((void*)&resp, P_LS2CL_REP_CHAR_DELETE_SUCC, sizeof(sP_LS2CL_REP_CHAR_DELETE_SUCC));
break;
}
case P_CL2LS_REQ_CHAR_SELECT: {
if (data->size != sizeof(sP_CL2LS_REQ_CHAR_SELECT))
return;
// character selected
sP_CL2LS_REQ_CHAR_SELECT* chararacter = (sP_CL2LS_REQ_CHAR_SELECT*)data->buf;
INITSTRUCT(sP_LS2CL_REP_CHAR_SELECT_SUCC, resp);
DEBUGLOG(
std::cout << "P_CL2LS_REQ_CHAR_SELECT:" << std::endl;
std::cout << "\tPC_UID: " << chararacter->iPC_UID << std::endl;
)
loginSessions[sock].selectedChar = chararacter->iPC_UID;
Database::updateSelected(loginSessions[sock].userID, loginSessions[sock].characters[chararacter->iPC_UID].slot);
sock->sendPacket((void*)&resp, P_LS2CL_REP_CHAR_SELECT_SUCC, sizeof(sP_LS2CL_REP_CHAR_SELECT_SUCC));
break;
}
case P_CL2LS_REQ_SHARD_SELECT: {
if (data->size != sizeof(sP_CL2LS_REQ_SHARD_SELECT))
return;
// tell client to connect to the shard server
sP_CL2LS_REQ_SHARD_SELECT* shard = (sP_CL2LS_REQ_SHARD_SELECT*)data->buf;
INITSTRUCT(sP_LS2CL_REP_SHARD_SELECT_SUCC, resp);
DEBUGLOG(
std::cout << "P_CL2LS_REQ_SHARD_SELECT:" << std::endl;
std::cout << "\tShard: " << (int)shard->ShardNum << std::endl;
)
const char* SHARD_IP = settings::SHARDSERVERIP.c_str();
resp.iEnterSerialKey = rand();
resp.g_FE_ServerPort = settings::SHARDPORT;
// copy IP to resp (this struct uses ASCII encoding so we don't have to goof around converting encodings)
memcpy(resp.g_FE_ServerIP, SHARD_IP, strlen(SHARD_IP));
resp.g_FE_ServerIP[strlen(SHARD_IP)] = '\0';
// pass player to CNSharedData
CNSharedData::setPlayer(resp.iEnterSerialKey, loginSessions[sock].characters[loginSessions[sock].selectedChar]);
sock->sendPacket((void*)&resp, P_LS2CL_REP_SHARD_SELECT_SUCC, sizeof(sP_LS2CL_REP_SHARD_SELECT_SUCC));
break;
}
case P_CL2LS_REQ_SAVE_CHAR_TUTOR: {
if (data->size != sizeof(sP_CL2LS_REQ_SAVE_CHAR_TUTOR))
return;
sP_CL2LS_REQ_SAVE_CHAR_TUTOR* save = (sP_CL2LS_REQ_SAVE_CHAR_TUTOR*)data->buf;
Database::finishTutorial(save->iPC_UID);
loginSessions[sock].characters[save->iPC_UID].PCStyle2.iTutorialFlag = 1;
loginSessions[sock].characters[save->iPC_UID].Equip[0].iID = 328;
loginSessions[sock].characters[save->iPC_UID].Equip[0].iType = 0;
loginSessions[sock].characters[save->iPC_UID].Equip[0].iOpt = 1;
break;
}
case P_CL2LS_REQ_CHANGE_CHAR_NAME: {
if (data->size != sizeof(sP_CL2LS_REQ_CHANGE_CHAR_NAME))
return;
sP_CL2LS_REQ_CHANGE_CHAR_NAME* save = (sP_CL2LS_REQ_CHANGE_CHAR_NAME*)data->buf;
Database::changeName(save);
INITSTRUCT(sP_LS2CL_REP_CHANGE_CHAR_NAME_SUCC, resp);
resp.iPC_UID = save->iPCUID;
memcpy(resp.szFirstName, save->szFirstName, sizeof(char16_t)*9);
memcpy(resp.szLastName, save->szLastName, sizeof(char16_t) * 17);
resp.iSlotNum = save->iSlotNum;
sock->sendPacket((void*)&resp, P_LS2CL_REP_CHANGE_CHAR_NAME_SUCC, sizeof(sP_LS2CL_REP_CHANGE_CHAR_NAME_SUCC));
break;
}
case P_CL2LS_REQ_PC_EXIT_DUPLICATE:{
if (data->size != sizeof(sP_CL2LS_REQ_PC_EXIT_DUPLICATE))
return;
sP_CL2LS_REQ_PC_EXIT_DUPLICATE* exit = (sP_CL2LS_REQ_PC_EXIT_DUPLICATE*)data->buf;
auto account = Database::findAccount(U16toU8(exit->szID));
if (account == nullptr)
break;
int accountId = account->AccountID;
if (!exitDuplicate(accountId))
PlayerManager::exitDuplicate(accountId);
break;
}
default:
if (settings::VERBOSITY)
std::cerr << "OpenFusion: LOGIN UNIMPLM ERR. PacketType: " << Defines::p2str(CL2LS, data->type) << " (" << data->type << ")" << std::endl;
break;
/* Unimplemented CL2LS packets:
P_CL2LS_CHECK_NAME_LIST - unused by the client
P_CL2LS_REQ_SERVER_SELECT
P_CL2LS_REQ_SHARD_LIST_INFO - dev commands, useless as we only run 1 server
*/
}
}
void CNLoginServer::newConnection(CNSocket* cns) {
cns->setActiveKey(SOCKETKEY_E); // by default they accept keys encrypted with the default key
}
void CNLoginServer::killConnection(CNSocket* cns) {
loginSessions.erase(cns);
}
#pragma region helperMethods
bool CNLoginServer::isAccountInUse(int accountId) {
std::map<CNSocket*, CNLoginData>::iterator it;
for (it = CNLoginServer::loginSessions.begin(); it != CNLoginServer::loginSessions.end(); it++)
{
if (it->second.userID == accountId)
return true;
}
return false;
}
bool CNLoginServer::exitDuplicate(int accountId)
{
std::map<CNSocket*, CNLoginData>::iterator it;
for (it = CNLoginServer::loginSessions.begin(); it != CNLoginServer::loginSessions.end(); it++)
{
if (it->second.userID == accountId)
{
CNSocket* sock = it->first;
INITSTRUCT(sP_LS2CL_REP_PC_EXIT_DUPLICATE, resp);
resp.iErrorCode = 0;
sock->sendPacket((void*)&resp, P_LS2CL_REP_PC_EXIT_DUPLICATE, sizeof(sP_LS2CL_REP_PC_EXIT_DUPLICATE));
sock->kill();
return true;
}
}
return false;
}
bool CNLoginServer::isLoginDataGood(std::string login, std::string password)
{
std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
return (std::regex_match(login, loginRegex) && std::regex_match(password, passwordRegex));
}
bool CNLoginServer::isPasswordCorrect(std::string actualPassword, std::string tryPassword)
{
return BCrypt::validatePassword(tryPassword, actualPassword);
}
#pragma endregion helperMethods

View File

@@ -1,43 +0,0 @@
#pragma once
#include "CNProtocol.hpp"
#include "Defines.hpp"
#include "Player.hpp"
#include <map>
struct CNLoginData {
std::map<int64_t, Player> characters;
int64_t selectedChar;
int userID; int slot;
};
enum class LOGINERRORID {
database_error = 0,
id_doesnt_exist = 1,
id_and_password_do_not_match = 2,
id_already_in_use = 3,
login_error = 4,
client_version_outdated = 6,
you_are_not_an_authorized_beta_tester = 7,
authentication_connection_error = 8,
updated_euala_required = 9
};
// WARNING: THERE CAN ONLY BE ONE OF THESE SERVERS AT A TIME!!!!!! TODO: change loginSessions & packet handlers to be non-static
class CNLoginServer : public CNServer {
private:
static void handlePacket(CNSocket* sock, CNPacketData* data);
static std::map<CNSocket*, CNLoginData> loginSessions;
static bool isLoginDataGood(std::string login, std::string password);
static bool isPasswordCorrect(std::string actualPassword, std::string tryPassword);
static bool isAccountInUse(int accountId);
//returns true if success
static bool exitDuplicate(int accountId);
public:
CNLoginServer(uint16_t p);
void newConnection(CNSocket* cns);
void killConnection(CNSocket* cns);
};

View File

@@ -1,373 +0,0 @@
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
// ========================================================[[ CNSocketEncryption ]]========================================================
// literally C/P from the client and converted to C++ (does some byte swapping /shrug)
int CNSocketEncryption::Encrypt_byte_change_A(int ERSize, uint8_t* data, int size) {
int num = 0;
int num2 = 0;
int num3 = 0;
while (num + ERSize <= size)
{
int num4 = num + num3;
int num5 = num + (ERSize - 1 - num3);
uint8_t b = data[num4];
data[num4] = data[num5];
data[num5] = b;
num += ERSize;
num3++;
if (num3 > ERSize / 2)
{
num3 = 0;
}
}
num2 = ERSize - (num + ERSize - size);
return num + num2;
}
int CNSocketEncryption::xorData(uint8_t* buffer, uint8_t* key, int size) {
// xor every 8 bytes with 8 byte key
for (int i = 0; i < size; i++) {
buffer[i] ^= key[i % keyLength];
}
return size;
}
uint64_t CNSocketEncryption::createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2) {
uint64_t num = (uint64_t)(iv1 + 1);
uint64_t num2 = (uint64_t)(iv2 + 1);
uint64_t dEKey = (uint64_t)(*(uint64_t*)&defaultKey[0]);
return dEKey * (uTime * num * num2);
}
int CNSocketEncryption::encryptData(uint8_t* buffer, uint8_t* key, int size) {
int eRSize = size % (keyLength / 2 + 1) * 2 + keyLength; // C/P from client
int size2 = xorData(buffer, key, size);
return Encrypt_byte_change_A(eRSize, buffer, size2);
}
int CNSocketEncryption::decryptData(uint8_t* buffer, uint8_t* key, int size) {
int eRSize = size % (keyLength / 2 + 1) * 2 + keyLength; // size % of 18????
int size2 = Encrypt_byte_change_A(eRSize, buffer, size);
return xorData(buffer, key, size2);
}
// ========================================================[[ CNPacketData ]]========================================================
CNPacketData::CNPacketData(void* b, uint32_t t, int l): buf(b), size(l), type(t) {}
// ========================================================[[ CNSocket ]]========================================================
CNSocket::CNSocket(SOCKET s, PacketHandler ph): sock(s), pHandler(ph) {
EKey = (uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]);
}
bool CNSocket::sendData(uint8_t* data, int size) {
int sentBytes = 0;
int maxTries = 10;
while (sentBytes < size) {
int sent = send(sock, (buffer_t*)(data + sentBytes), size - sentBytes, 0); // no flags defined
if (SOCKETERROR(sent)) {
if (OF_ERRNO == OF_EWOULD && maxTries > 0) {
maxTries--;
continue; // try again
}
std::cout << "[FATAL] SOCKET ERROR: " << OF_ERRNO << std::endl;
return false; // error occured while sending bytes
}
sentBytes += sent;
}
return true; // it worked!
}
void CNSocket::setEKey(uint64_t k) {
EKey = k;
}
void CNSocket::setFEKey(uint64_t k) {
FEKey = k;
}
uint64_t CNSocket::getEKey() {
return EKey;
}
uint64_t CNSocket::getFEKey() {
return FEKey;
}
bool CNSocket::isAlive() {
return alive;
}
void CNSocket::kill() {
alive = false;
#ifdef _WIN32
shutdown(sock, SD_BOTH);
closesocket(sock);
#else
shutdown(sock, SHUT_RDWR);
close(sock);
#endif
}
// we don't own buf, TODO: queue packets up to send in step()
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;
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);
// 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;
}
}
// send packet data!
if (alive && !sendData(fullpkt, bodysize+4))
kill();
free(fullpkt);
}
void CNSocket::setActiveKey(ACTIVEKEY key) {
activeKey = key;
}
void CNSocket::step() {
// read step
if (readSize <= 0) {
// we aren't reading a packet yet, try to start looking for one
int recved = recv(sock, (buffer_t*)readBuffer, sizeof(int32_t), 0);
if (!SOCKETERROR(recved)) {
// we got out packet size!!!!
readSize = *((int32_t*)readBuffer);
// sanity check
if (readSize > CN_PACKET_BUFFER_SIZE) {
kill();
return;
}
// we'll just leave bufferIndex at 0 since we already have the packet size, it's safe to overwrite those bytes
activelyReading = true;
} else if (OF_ERRNO != OF_EWOULD) {
// serious socket issue, disconnect connection
kill();
return;
}
}
if (readSize > 0 && readBufferIndex < readSize) {
// read until the end of the packet! (or at least try too)
int recved = recv(sock, (buffer_t*)(readBuffer + readBufferIndex), readSize - readBufferIndex, 0);
if (!SOCKETERROR(recved))
readBufferIndex += recved;
else if (OF_ERRNO != OF_EWOULD) {
// serious socket issue, disconnect connection
kill();
return;
}
}
if (activelyReading && readBufferIndex - readSize <= 0) {
// 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);
// reset vars :)
readSize = 0;
readBufferIndex = 0;
activelyReading = false;
}
}
// ========================================================[[ CNServer ]]========================================================
void CNServer::init() {
// create socket file descriptor
sock = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKETINVALID(sock)) {
std::cerr << "[FATAL] OpenFusion: socket failed" << std::endl;
exit(EXIT_FAILURE);
}
// attach socket to the port
int opt = 1;
#ifdef _WIN32
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)) != 0) {
#else
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) {
#endif
std::cerr << "[FATAL] OpenFusion: setsockopt failed" << std::endl;
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(port);
addressSize = sizeof(address);
// Bind to the port
if (SOCKETERROR(bind(sock, (struct sockaddr *)&address, addressSize))) {
std::cerr << "[FATAL] OpenFusion: bind failed" << std::endl;
exit(EXIT_FAILURE);
}
if (SOCKETERROR(listen(sock, SOMAXCONN))) {
std::cerr << "[FATAL] OpenFusion: listen failed" << std::endl;
exit(EXIT_FAILURE);
}
// set server listener to non-blocking
#ifdef _WIN32
unsigned long mode = 1;
if (ioctlsocket(sock, FIONBIO, &mode) != 0) {
#else
if (fcntl(sock, F_SETFL, (fcntl(sock, F_GETFL, 0) | O_NONBLOCK)) != 0) {
#endif
std::cerr << "[FATAL] OpenFusion: fcntl failed" << std::endl;
exit(EXIT_FAILURE);
}
}
CNServer::CNServer() {};
CNServer::CNServer(uint16_t p): port(p) {}
void CNServer::start() {
std::cout << "Starting server at *:" << port << std::endl;
// listen to new connections, add to connection list
while (active) {
std::lock_guard<std::mutex> lock(activeCrit);
// listen for a new connection
SOCKET newConnectionSocket = accept(sock, (struct sockaddr *)&(address), (socklen_t*)&(addressSize));
if (!SOCKETINVALID(newConnectionSocket)) {
// new connection! make sure to set non-blocking!
#ifdef _WIN32
unsigned long mode = 1;
if (ioctlsocket(newConnectionSocket, FIONBIO, &mode) != 0) {
#else
if (fcntl(newConnectionSocket, F_SETFL, (fcntl(sock, F_GETFL, 0) | O_NONBLOCK)) != 0) {
#endif
std::cerr << "[WARN] OpenFusion: fcntl failed on new connection" << std::endl;
#ifdef _WIN32
shutdown(newConnectionSocket, SD_BOTH);
closesocket(newConnectionSocket);
#else
shutdown(newConnectionSocket, SHUT_RDWR);
close(newConnectionSocket);
#endif
continue;
}
std::cout << "New connection! " << inet_ntoa(address.sin_addr) << std::endl;
// add connection to list!
CNSocket* tmp = new CNSocket(newConnectionSocket, pHandler);
connections.push_back(tmp);
newConnection(tmp);
}
// for each connection, check if it's alive, if not kill it!
std::list<CNSocket*>::iterator i = connections.begin();
while (i != connections.end()) {
CNSocket* cSock = *i;
if (cSock->isAlive()) {
cSock->step();
++i; // go to the next element
} else {
killConnection(cSock);
connections.erase(i++);
delete cSock;
}
}
onStep();
#ifdef _WIN32
Sleep(0);
#else
sleep(0); // so your cpu isn't at 100% all the time, we don't need all of that! im not hacky! you're hacky!
#endif
}
}
void CNServer::kill() {
std::lock_guard<std::mutex> lock(activeCrit); // the lock will be removed when the function ends
active = false;
// kill all connections
std::list<CNSocket*>::iterator i = connections.begin();
while (i != connections.end()) {
CNSocket* cSock = *i;
if (cSock->isAlive()) {
cSock->kill();
}
++i; // go to the next element
delete cSock;
}
connections.clear();
}
void CNServer::printPacket(CNPacketData *data, int type) {
if (settings::VERBOSITY < 2)
return;
if (settings::VERBOSITY < 3) switch (data->type) {
case P_CL2LS_REP_LIVE_CHECK:
case P_CL2FE_REP_LIVE_CHECK:
case P_CL2FE_REQ_PC_MOVE:
case P_CL2FE_REQ_PC_JUMP:
case P_CL2FE_REQ_PC_SLOPE:
case P_CL2FE_REQ_PC_MOVEPLATFORM:
case P_CL2FE_REQ_PC_MOVETRANSPORTATION:
case P_CL2FE_REQ_PC_ZIPLINE:
case P_CL2FE_REQ_PC_JUMPPAD:
case P_CL2FE_REQ_PC_LAUNCHER:
case P_CL2FE_REQ_PC_STOP:
return;
}
std::cout << "OpenFusion: received " << Defines::p2str(type, data->type) << " (" << data->type << ")" << std::endl;
}
void CNServer::newConnection(CNSocket* cns) {} // stubbed
void CNServer::killConnection(CNSocket* cns) {} // stubbed
void CNServer::onStep() {} // stubbed

View File

@@ -1,78 +0,0 @@
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include "CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "CNShared.hpp"
#include "settings.hpp"
#include <iostream>
#include <sstream>
#include <cstdlib>
std::map<uint32_t, PacketHandler> CNShardServer::ShardPackets;
std::list<TimerEvent> CNShardServer::Timers;
CNShardServer::CNShardServer(uint16_t p) {
port = p;
pHandler = &CNShardServer::handlePacket;
REGISTER_SHARD_TIMER(keepAliveTimer, 2000);
init();
}
void CNShardServer::handlePacket(CNSocket* sock, CNPacketData* data) {
printPacket(data, CL2FE);
if (ShardPackets.find(data->type) != ShardPackets.end())
ShardPackets[data->type](sock, data);
else if (settings::VERBOSITY > 0)
std::cerr << "OpenFusion: SHARD UNIMPLM ERR. PacketType: " << Defines::p2str(CL2FE, data->type) << " (" << data->type << ")" << std::endl;
}
void CNShardServer::keepAliveTimer(CNServer* serv, uint64_t currTime) {
auto cachedPlayers = PlayerManager::players;
for (auto pair : cachedPlayers) {
if (pair.second.lastHeartbeat != 0 && currTime - pair.second.lastHeartbeat > 60000) { // if the client hadn't responded in 60 seconds, its a dead connection so throw it out
pair.first->kill();
continue;
}
// passed the heartbeat, send another
INITSTRUCT(sP_FE2CL_REQ_LIVE_CHECK, data);
pair.first->sendPacket((void*)&data, P_FE2CL_REQ_LIVE_CHECK, sizeof(sP_FE2CL_REQ_LIVE_CHECK));
}
}
void CNShardServer::newConnection(CNSocket* cns) {
cns->setActiveKey(SOCKETKEY_E); // by default they accept keys encrypted with the default key
}
void CNShardServer::killConnection(CNSocket* cns) {
// check if the player ever sent a REQ_PC_ENTER
if (PlayerManager::players.find(cns) == PlayerManager::players.end())
return;
// remove from CNSharedData
int64_t key = PlayerManager::getPlayer(cns)->SerialKey;
PlayerManager::removePlayer(cns);
CNSharedData::erasePlayer(key);
}
void CNShardServer::onStep() {
uint64_t currTime = getTime();
for (TimerEvent& event : Timers) {
if (event.scheduledEvent == 0) {
// event hasn't been queued yet, go ahead and do that
event.scheduledEvent = currTime + event.delta;
continue;
}
if (event.scheduledEvent < currTime) {
// timer needs to be called
event.handlr(this, currTime);
event.scheduledEvent = currTime + event.delta;
}
}
}

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

View File

@@ -1,36 +0,0 @@
#include "CNStructs.hpp"
#if defined _MSC_VER
#include <chrono>
#endif
std::string U16toU8(char16_t* src) {
try {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
return convert.to_bytes(src);
} catch(std::exception e) {
return "";
}
}
// returns number of char16_t that was written at des
size_t U8toU16(std::string src, char16_t* des) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
std::u16string tmp = convert.from_bytes(src);
// copy utf16 string to buffer
memcpy(des, tmp.c_str(), sizeof(char16_t) * tmp.length());
des[tmp.length()] = '\0';
return tmp.length();
}
uint64_t getTime() {
#ifndef _MSC_VER
struct timeval tp;
gettimeofday(&tp, NULL);
return tp.tv_sec * 1000 + tp.tv_usec / 1000;
#else
std::chrono::milliseconds value = std::chrono::duration_cast<std::chrono::milliseconds>((std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now())).time_since_epoch());
return (uint64_t)(value.count());
#endif
}

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,67 +0,0 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "ChatManager.hpp"
#include "PlayerManager.hpp"
void ChatManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_FREECHAT_MESSAGE, chatHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT, emoteHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE, menuChatHandler);
}
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;
PlayerView plr = PlayerManager::players[sock];
// send to client
INITSTRUCT(sP_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC, resp);
memcpy(resp.szFreeChat, chat->szFreeChat, sizeof(chat->szFreeChat));
resp.iPC_ID = plr.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
for (CNSocket* otherSock : plr.viewable) {
otherSock->sendPacket((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;
PlayerView plr = PlayerManager::players[sock];
// send to client
INITSTRUCT(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, resp);
memcpy(resp.szFreeChat, chat->szFreeChat, sizeof(chat->szFreeChat));
resp.iPC_ID = PlayerManager::players[sock].plr->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
for (CNSocket* otherSock : plr.viewable) {
otherSock->sendPacket((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;
PlayerView plr = PlayerManager::players[sock];
// send to client
INITSTRUCT(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, resp);
resp.iEmoteCode = emote->iEmoteCode;
resp.iID_From = plr.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)
for (CNSocket* otherSock : plr.viewable) {
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT));
}
}

View File

@@ -1,11 +0,0 @@
#pragma once
#include "CNShardServer.hpp"
namespace ChatManager {
void init();
void chatHandler(CNSocket* sock, CNPacketData* data);
void emoteHandler(CNSocket* sock, CNPacketData* data);
void menuChatHandler(CNSocket* sock, CNPacketData* data);
}

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

View File

@@ -1,131 +0,0 @@
#include "CombatManager.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "ItemManager.hpp"
#include <assert.h>
void CombatManager::init() {
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);
}
void CombatManager::pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_REQ_PC_ATTACK_NPCs* pkt = (sP_CL2FE_REQ_PC_ATTACK_NPCs*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
// sanity check
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_NPCs), pkt->iNPCCnt, sizeof(int32_t), data->size)) {
std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_NPCs packet size\n";
return;
}
int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_NPCs));
/*
* 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_ATTACK_NPCs_SUCC), pkt->iNPCCnt, sizeof(sAttackResult))) {
std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_NPCs_SUCC packet size\n";
return;
}
// initialize response struct
size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC) + pkt->iNPCCnt * sizeof(sAttackResult);
uint8_t respbuf[4096];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_ATTACK_NPCs_SUCC *resp = (sP_FE2CL_PC_ATTACK_NPCs_SUCC*)respbuf;
sAttackResult *respdata = (sAttackResult*)(respbuf+sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC));
resp->iNPCCnt = pkt->iNPCCnt;
for (int i = 0; i < pkt->iNPCCnt; i++) {
if (NPCManager::NPCs.find(pktdata[i]) == NPCManager::NPCs.end()) {
// not sure how to best handle this
std::cout << "[WARN] pcAttackNpcs: mob ID not found" << std::endl;
return;
}
BaseNPC& mob = NPCManager::NPCs[pktdata[i]];
mob.appearanceData.iHP -= 100;
if (mob.appearanceData.iHP <= 0)
giveReward(sock);
// TODO: despawn mobs when they die
respdata[i].iID = mob.appearanceData.iNPC_ID;
respdata[i].iDamage = 100;
respdata[i].iHP = mob.appearanceData.iHP;
respdata[i].iHitFlag = 2;
}
sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_NPCs_SUCC, resplen);
// a bit of a hack: these are the same size, so we can reuse the output packet
assert(sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC) == sizeof(sP_FE2CL_PC_ATTACK_NPCs));
sP_FE2CL_PC_ATTACK_NPCs *resp1 = (sP_FE2CL_PC_ATTACK_NPCs*)respbuf;
resp1->iPC_ID = plr->iID;
// send to other players
for (CNSocket *s : PlayerManager::players[sock].viewable) {
if (s == sock)
continue;
s->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_NPCs, resplen);
}
}
void CombatManager::combatBegin(CNSocket *sock, CNPacketData *data) {} // stub
void CombatManager::combatEnd(CNSocket *sock, CNPacketData *data) {} // stub
void CombatManager::dotDamageOnOff(CNSocket *sock, CNPacketData *data) {} // stub
void CombatManager::giveReward(CNSocket *sock) {
Player *plr = PlayerManager::getPlayer(sock);
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
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);
// update player
plr->money += 50;
plr->fusionmatter += 70;
// simple rewards
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
int slot = ItemManager::findFreeSlot(plr);
if (slot == -1) {
// no room for an item, but you still get FM and taros
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, sizeof(sP_FE2CL_REP_REWARD_ITEM));
} else {
// item reward
item->sItem.iType = 9;
item->sItem.iID = 1;
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);
}
}

View File

@@ -1,16 +0,0 @@
#pragma once
#include "CNProtocol.hpp"
#include "CNShared.hpp"
#include "CNShardServer.hpp"
namespace CombatManager {
void init();
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 giveReward(CNSocket *sock);
}

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

View File

@@ -1,357 +0,0 @@
#include "Database.hpp"
#include "contrib/bcrypt/BCrypt.hpp"
#include "CNProtocol.hpp"
#include <string>
#include "contrib/JSON.hpp"
#include "CNStructs.hpp"
#include "settings.hpp"
#include "Player.hpp"
#include "CNStructs.hpp"
#include "contrib/sqlite/sqlite_orm.h"
using namespace sqlite_orm;
# pragma region DatabaseScheme
auto db = make_storage("database.db",
make_table("Accounts",
make_column("AccountID", &Database::Account::AccountID, autoincrement(), primary_key()),
make_column("Login", &Database::Account::Login),
make_column("Password", &Database::Account::Password),
make_column("Selected", &Database::Account::Selected)
),
make_table("Players",
make_column("PlayerID", &Database::DbPlayer::PlayerID, autoincrement(), primary_key()),
make_column("AccountID", &Database::DbPlayer::AccountID),
make_column("Slot", &Database::DbPlayer::slot),
make_column("Firstname", &Database::DbPlayer::FirstName),
make_column("LastName", &Database::DbPlayer::LastName),
make_column("Level", &Database::DbPlayer::Level),
make_column("AppearanceFlag", &Database::DbPlayer::AppearanceFlag),
make_column("TutorialFlag", &Database::DbPlayer::TutorialFlag),
make_column("PayZoneFlag", &Database::DbPlayer::PayZoneFlag),
make_column("XCoordinates", &Database::DbPlayer::x_coordinates),
make_column("YCoordinates", &Database::DbPlayer::y_coordinates),
make_column("ZCoordinates", &Database::DbPlayer::z_coordinates),
make_column("Body", &Database::DbPlayer::Body),
make_column("Class", &Database::DbPlayer::Class),
make_column("EquipFoot", &Database::DbPlayer::EquipFoot),
make_column("EquipLB", &Database::DbPlayer::EquipLB),
make_column("EquipUB", &Database::DbPlayer::EquipUB),
make_column("EquipWeapon1", &Database::DbPlayer::EquipWeapon1),
make_column("EyeColor", &Database::DbPlayer::EyeColor),
make_column("FaceStyle", &Database::DbPlayer::FaceStyle),
make_column("Gender", &Database::DbPlayer::Gender),
make_column("HP", &Database::DbPlayer::HP),
make_column("HairColor", &Database::DbPlayer::HairColor),
make_column("HairStyle", &Database::DbPlayer::HairStyle),
make_column("Height", &Database::DbPlayer::Height),
make_column("NameCheck", &Database::DbPlayer::NameCheck),
make_column("SkinColor", &Database::DbPlayer::SkinColor),
make_column("isGM", &Database::DbPlayer::isGM),
make_column("FusionMatter", &Database::DbPlayer::FusionMatter),
make_column("Taros", &Database::DbPlayer::Taros)
),
make_table("Inventory",
make_column("AccountID", &Database::Inventory::AccountID, primary_key())
)
);
# pragma endregion DatabaseScheme
#pragma region LoginServer
void Database::open()
{
//this parameter means it will try to preserve data during migration
bool preserve = true;
db.sync_schema(preserve);
DEBUGLOG(
std::cout << "[DB] Database in operation" << std::endl;
)
}
int Database::addAccount(std::string login, std::string password)
{
password = BCrypt::generateHash(password);
Account x = {};
x.Login = login;
x.Password = password;
x.Selected = 1;
return db.insert(x);
}
void Database::updateSelected(int accountId, int slot)
{
Account acc = db.get<Account>(accountId);
acc.Selected = slot;
db.update(acc);
}
std::unique_ptr<Database::Account> Database::findAccount(std::string login)
{
//this is awful, I've tried everything to improve it
auto find = db.get_all<Account>(
where(c(&Account::Login) == login), limit(1));
if (find.empty())
return nullptr;
return
std::unique_ptr<Account>(new Account(find.front()));
}
bool Database::isNameFree(sP_CL2LS_REQ_CHECK_CHAR_NAME* nameCheck)
{
//TODO: add colate nocase
std::string First = U16toU8(nameCheck->szFirstName);
std::string Last = U16toU8(nameCheck->szLastName);
return
(db.get_all<DbPlayer>
(where((c(&DbPlayer::FirstName) == First)
and (c(&DbPlayer::LastName) == Last)))
.empty());
}
int Database::createCharacter(sP_CL2LS_REQ_SAVE_CHAR_NAME* save, int AccountID)
{
DbPlayer create = {};
//save packet data
create.FirstName = U16toU8(save->szFirstName);
create.LastName = U16toU8(save->szLastName);
create.slot = save->iSlotNum;
create.AccountID = AccountID;
//set flags
create.AppearanceFlag = 0;
create.TutorialFlag = 0;
create.PayZoneFlag = 0;
//set namecheck based on setting
if (settings::APPROVEALLNAMES || save->iFNCode)
create.NameCheck = 1;
else
create.NameCheck = 0;
//create default body character
create.Body= 0;
create.Class= 0;
create.EquipFoot= 0;
create.EquipLB= 0;
create.EquipUB= 0;
create.EquipWeapon1= 0;
create.EquipWeapon2= 0;
create.EyeColor= 1;
create.FaceStyle= 1;
create.Gender= 1;
create.HP= 1000;
create.HairColor= 1;
create.HairStyle = 1;
create.Height= 0;
create.Level= 1;
create.SkinColor= 1;
create.isGM = false;
//commented and disabled for now
//if (U16toU8(save->szFirstName) == settings::GMPASS) {
// create.isGM = true;
//}
create.FusionMatter= 0;
create.Taros= 0;
create.x_coordinates = settings::SPAWN_X;
create.y_coordinates= settings::SPAWN_Y;
create.z_coordinates= settings::SPAWN_Z;
return db.insert(create);
}
void Database::finishCharacter(sP_CL2LS_REQ_CHAR_CREATE* character)
{
DbPlayer finish = getDbPlayerById(character->PCStyle.iPC_UID);
finish.AppearanceFlag = 1;
finish.Body = character->PCStyle.iBody;
finish.Class = character->PCStyle.iClass;
finish.EquipFoot = character->sOn_Item.iEquipFootID;
finish.EquipLB = character->sOn_Item.iEquipLBID;
finish.EquipUB = character->sOn_Item.iEquipUBID;
finish.EyeColor = character->PCStyle.iEyeColor;
finish.FaceStyle = character->PCStyle.iFaceStyle;
finish.Gender = character->PCStyle.iGender;
finish.HairColor = character->PCStyle.iHairColor;
finish.HairStyle = character->PCStyle.iHairStyle;
finish.Height = character->PCStyle.iHeight;
finish.Level = 1;
finish.SkinColor = character->PCStyle.iSkinColor;
db.update(finish);
}
void Database::finishTutorial(int PlayerID)
{
DbPlayer finish = getDbPlayerById(PlayerID);
finish.TutorialFlag = 1;
//equip lightning gun
finish.EquipWeapon1 = 328;
db.update(finish);
}
int Database::deleteCharacter(int characterID)
{
auto find =
db.get_all<DbPlayer>(where(c(&DbPlayer::PlayerID) == characterID));
int slot = find.front().slot;
db.remove<DbPlayer>(find.front().PlayerID);
return slot;
}
std::vector <Player> Database::getCharacters(int UserID)
{
std::vector<DbPlayer>characters =
db.get_all<DbPlayer>(where
(c(&DbPlayer::AccountID) == UserID));
//parsing DbPlayer to Player
std::vector<Player> result = std::vector<Player>();
for (auto &character : characters) {
Player toadd = DbToPlayer(character);
result.push_back(
toadd
);
}
return result;
}
void Database::evaluateCustomName(int characterID, CUSTOMNAME decision) {
DbPlayer player = getDbPlayerById(characterID);
player.NameCheck = (int)decision;
db.update(player);
}
void Database::changeName(sP_CL2LS_REQ_CHANGE_CHAR_NAME* save) {
DbPlayer Player = getDbPlayerById(save->iPCUID);
Player.FirstName = U16toU8(save->szFirstName);
Player.LastName = U16toU8(save->szLastName);
if (settings::APPROVEALLNAMES || save->iFNCode)
Player.NameCheck = 1;
else
Player.NameCheck = 0;
db.update(Player);
}
Database::DbPlayer Database::playerToDb(Player player)
{
DbPlayer result = {}; // fixes some weird memory errors, this zeros out the members (not the padding inbetween though)
result.PlayerID = player.iID;
result.AccountID = player.accountId;
result.AppearanceFlag = player.PCStyle2.iAppearanceFlag;
result.Body = player.PCStyle.iBody;
result.Class = player.PCStyle.iClass;
//equipment
result.EyeColor = player.PCStyle.iEyeColor;
result.FaceStyle = player.PCStyle.iFaceStyle;
result.FirstName = U16toU8( player.PCStyle.szFirstName);
//fm
result.Gender = player.PCStyle.iGender;
result.HairColor = player.PCStyle.iHairColor;
result.HairStyle = player.PCStyle.iHairStyle;
result.Height = player.PCStyle.iHeight;
result.HP = player.HP;
result.isGM = player.IsGM;
result.LastName = U16toU8(player.PCStyle.szLastName);
result.Level = player.level;
result.NameCheck = player.PCStyle.iNameCheck;
result.PayZoneFlag = player.PCStyle2.iPayzoneFlag;
result.PlayerID = player.PCStyle.iPC_UID;
result.SkinColor = player.PCStyle.iSkinColor;
result.slot = player.slot;
//taros
result.TutorialFlag = player.PCStyle2.iTutorialFlag;
result.x_coordinates = player.x;
result.y_coordinates = player.y;
result.z_coordinates = player.z;
return result;
}
Player Database::DbToPlayer(DbPlayer player) {
Player result = {}; // fixes some weird memory errors, this zeros out the members (not the padding inbetween though)
result.accountId = player.AccountID;
result.PCStyle2.iAppearanceFlag = player.AppearanceFlag;
result.PCStyle.iBody = player.Body;
result.PCStyle.iClass = player.Class;
result.PCStyle.iEyeColor = player.EyeColor;
result.PCStyle.iFaceStyle = player.FaceStyle;
U8toU16(player.FirstName, result.PCStyle.szFirstName);
result.PCStyle.iGender = player.Gender;
result.PCStyle.iHairColor = player.HairColor;
result.PCStyle.iHairStyle = player.HairStyle;
result.PCStyle.iHeight = player.Height;
result.HP = player.HP;
result.IsGM = player.isGM;
U8toU16(player.LastName, result.PCStyle.szLastName);
result.level = player.Level;
result.PCStyle.iNameCheck = player.NameCheck;
result.PCStyle2.iPayzoneFlag = player.PayZoneFlag;
result.iID = player.PlayerID;
result.PCStyle.iPC_UID = player.PlayerID;
result.PCStyle.iSkinColor = player.SkinColor;
result.slot = player.slot;
result.PCStyle2.iTutorialFlag = player.TutorialFlag;
result.x = player.x_coordinates;
result.y = player.y_coordinates;
result.z = player.z_coordinates;
//TODO:: implement all of below
result.SerialKey = 0;
result.money = 0;
result.fusionmatter = 0;
result.activeNano = 0;
result.iPCState = 0;
result.equippedNanos[0] = 1;
result.equippedNanos[1] = 0;
result.equippedNanos[2] = 0;
result.isTrading = false;
result.isTradeConfirm = false;
result.Nanos[1].iID = 1;
result.Nanos[1].iSkillID = 1;
result.Nanos[1].iStamina = 150;
for (int i = 0; i < 37; i++) {
result.Nanos[i].iID = 0;
result.Nanos[i].iSkillID = 0;
result.Nanos[i].iStamina = 0;
}
result.Equip[0].iID = player.EquipWeapon1;
result.Equip[0].iType = 0;
(player.EquipWeapon1) ? result.Equip[0].iOpt = 1 : result.Equip[0].iOpt = 0;
result.Equip[1].iID = player.EquipUB;
result.Equip[1].iType = 1;
(player.EquipUB) ? result.Equip[1].iOpt = 1 : result.Equip[1].iOpt = 0;
result.Equip[2].iID = player.EquipLB;
result.Equip[2].iType = 2;
(player.EquipLB) ? result.Equip[2].iOpt = 1 : result.Equip[2].iOpt = 0;
result.Equip[3].iID = player.EquipFoot;
result.Equip[3].iType = 3;
(player.EquipFoot) ? result.Equip[3].iOpt = 1 : result.Equip[3].iOpt = 0;
for (int i = 4; i < AEQUIP_COUNT; i++) {
// empty equips
result.Equip[i].iID = 0;
result.Equip[i].iType = i;
result.Equip[i].iOpt = 0;
}
for (int i = 0; i < AINVEN_COUNT; i++) {
// setup inventories
result.Inven[i].iID = 0;
result.Inven[i].iType = 0;
result.Inven[i].iOpt = 0;
}
return result;
}
Database::DbPlayer Database::getDbPlayerById(int id) {
return db.get_all<DbPlayer>(where(c(&DbPlayer::PlayerID) == id))
.front();
}
#pragma endregion LoginServer

View File

@@ -1,91 +0,0 @@
#pragma once
#include "CNStructs.hpp"
#include "Player.hpp"
#include <string>
#include <vector>
namespace Database {
#pragma region DatabaseStructs
struct Account
{
int AccountID;
std::string Login;
std::string Password;
int Selected;
};
struct Inventory
{
int AccountID;
};
struct DbPlayer
{
int PlayerID;
int AccountID;
short int slot;
std::string FirstName;
std::string LastName;
short int AppearanceFlag;
short int Body;
short int Class;
short int EquipFoot;
short int EquipLB;
short int EquipUB;
short int EquipWeapon1;
short int EquipWeapon2;
short int EyeColor;
short int FaceStyle;
short int Gender;
int HP;
short int HairColor;
short int HairStyle;
short int Height;
short int Level;
short int NameCheck;
short int PayZoneFlag;
short int SkinColor;
bool TutorialFlag;
bool isGM;
int FusionMatter;
int Taros;
int x_coordinates;
int y_coordinates;
int z_coordinates;
};
#pragma endregion DatabaseStructs
//handles migrations
void open();
//returns ID
int addAccount(std::string login, std::string password);
void updateSelected(int accountId, int playerId);
std::unique_ptr<Account> findAccount(std::string login);
bool isNameFree(sP_CL2LS_REQ_CHECK_CHAR_NAME* nameCheck);
//called after chosing name, returns ID
int createCharacter(sP_CL2LS_REQ_SAVE_CHAR_NAME* save, int AccountID);
//called after finishing creation
void finishCharacter(sP_CL2LS_REQ_CHAR_CREATE* character);
//called after tutorial
void finishTutorial(int PlayerID);
//returns slot number
int deleteCharacter(int characterID);
std::vector <Player> getCharacters(int userID);
//accepting/declining custom name
enum class CUSTOMNAME {
approve = 1,
disapprove = 2
};
void evaluateCustomName(int characterID, CUSTOMNAME decision);
void changeName(sP_CL2LS_REQ_CHANGE_CHAR_NAME* save);
//parsing DbPlayer
DbPlayer playerToDb(Player player);
Player DbToPlayer(DbPlayer player);
//getting players
DbPlayer getDbPlayerById(int id);
}

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

334
src/Groups.cpp Normal file
View File

@@ -0,0 +1,334 @@
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Groups.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include <iostream>
#include <chrono>
#include <algorithm>
#include <thread>
/*
* NOTE: Variadic response packets that list group members are technically
* double-variadic, as they have two count members with trailing struct counts,
* and are thus incompatible with the generic sendPacket() wrapper.
* That means we still have to (carefully) use validOutVarPacket() in this
* source file.
*/
using namespace Groups;
static void requestGroup(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_GROUP_INVITE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_To);
if (otherPlr == nullptr)
return;
otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup);
if (otherPlr == nullptr)
return;
// fail if the group is full or the other player is already in a group
if (plr->groupCnt >= 4 || otherPlr->iIDGroup != otherPlr->iID || otherPlr->groupCnt > 1) {
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_FAIL, resp);
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_FAIL, sizeof(sP_FE2CL_PC_GROUP_INVITE_FAIL));
return;
}
CNSocket* otherSock = PlayerManager::getSockFromID(recv->iID_To);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE, resp);
resp.iHostID = plr->iID;
otherSock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE, sizeof(sP_FE2CL_PC_GROUP_INVITE));
}
static void refuseGroup(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_GROUP_INVITE_REFUSE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE_REFUSE*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(recv->iID_From);
if (otherSock == nullptr)
return;
Player* plr = PlayerManager::getPlayer(sock);
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_REFUSE, resp);
resp.iID_To = plr->iID;
otherSock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_REFUSE, sizeof(sP_FE2CL_PC_GROUP_INVITE_REFUSE));
}
static void joinGroup(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_GROUP_JOIN* recv = (sP_CL2FE_REQ_PC_GROUP_JOIN*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_From);
if (otherPlr == nullptr)
return;
otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup);
if (otherPlr == nullptr)
return;
// fail if the group is full or the other player is already in a group
if (plr->groupCnt > 1 || plr->iIDGroup != plr->iID || otherPlr->groupCnt >= 4) {
INITSTRUCT(sP_FE2CL_PC_GROUP_JOIN_FAIL, resp);
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_JOIN_FAIL, sizeof(sP_FE2CL_PC_GROUP_JOIN_FAIL));
return;
}
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), otherPlr->groupCnt + 1, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
return;
}
plr->iIDGroup = otherPlr->iID;
otherPlr->groupCnt += 1;
otherPlr->groupIDs[otherPlr->groupCnt-1] = plr->iID;
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + otherPlr->groupCnt * sizeof(sPCGroupMemberInfo);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_JOIN *resp = (sP_FE2CL_PC_GROUP_JOIN*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_JOIN));
resp->iID_NewMember = plr->iID;
resp->iMemberPCCnt = otherPlr->groupCnt;
int bitFlag = getGroupFlags(otherPlr);
for (int i = 0; i < otherPlr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]);
if (varPlr == nullptr || sockTo == nullptr)
continue;
respdata[i].iPC_ID = varPlr->iID;
respdata[i].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i].iSpecialState = varPlr->iSpecialState;
respdata[i].iLv = varPlr->level;
respdata[i].iHP = varPlr->HP;
respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level);
//respdata[i].iMapType = 0;
//respdata[i].iMapNum = 0;
respdata[i].iX = varPlr->x;
respdata[i].iY = varPlr->y;
respdata[i].iZ = varPlr->z;
// client doesnt read nano data here
if (varPlr != plr) { // apply the new member's buffs to the group and the group's buffs to the new member
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 1, 1, bitFlag);
if (Nanos::SkillTable[plr->Nanos[plr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 1, 1, bitFlag);
}
}
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_JOIN, resplen);
}
static void leaveGroup(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
groupKickPlayer(plr);
}
void Groups::sendToGroup(Player* plr, void* buf, uint32_t type, size_t size) {
for (int i = 0; i < plr->groupCnt; i++) {
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[i]);
if (sock == nullptr)
continue;
if (type == P_FE2CL_PC_GROUP_LEAVE_SUCC) {
Player* leavingPlr = PlayerManager::getPlayer(sock);
leavingPlr->iIDGroup = leavingPlr->iID;
}
sock->sendPacket(buf, type, size);
}
}
void Groups::groupTickInfo(Player* plr) {
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), plr->groupCnt, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
return;
}
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + plr->groupCnt * sizeof(sPCGroupMemberInfo);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_MEMBER_INFO *resp = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO));
resp->iID = plr->iID;
resp->iMemberPCCnt = plr->groupCnt;
for (int i = 0; i < plr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
if (varPlr == nullptr)
continue;
respdata[i].iPC_ID = varPlr->iID;
respdata[i].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i].iSpecialState = varPlr->iSpecialState;
respdata[i].iLv = varPlr->level;
respdata[i].iHP = varPlr->HP;
respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level);
//respdata[i].iMapType = 0;
//respdata[i].iMapNum = 0;
respdata[i].iX = varPlr->x;
respdata[i].iY = varPlr->y;
respdata[i].iZ = varPlr->z;
if (varPlr->activeNano > 0) {
respdata[i].bNano = 1;
respdata[i].Nano = varPlr->Nanos[varPlr->activeNano];
}
}
sendToGroup(plr, (void*)&respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO, resplen);
}
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);
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1);
sendToGroup(plr, (void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
plr->groupCnt = 1;
return;
}
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), otherPlr->groupCnt - 1, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size\n";
return;
}
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_LEAVE) + (otherPlr->groupCnt - 1) * sizeof(sPCGroupMemberInfo);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_LEAVE *resp = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_LEAVE));
resp->iID_LeaveMember = plr->iID;
resp->iMemberPCCnt = otherPlr->groupCnt - 1;
int bitFlag = getGroupFlags(otherPlr) & ~plr->iGroupConditionBitFlag;
int moveDown = 0;
CNSocket* sock = PlayerManager::getSockFromID(plr->iID);
if (sock == nullptr)
return;
for (int i = 0; i < otherPlr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]);
if (varPlr == nullptr || sockTo == nullptr)
continue;
if (moveDown == 1)
otherPlr->groupIDs[i-1] = otherPlr->groupIDs[i];
respdata[i-moveDown].iPC_ID = varPlr->iID;
respdata[i-moveDown].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i-moveDown].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i-moveDown].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i-moveDown].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i-moveDown].iSpecialState = varPlr->iSpecialState;
respdata[i-moveDown].iLv = varPlr->level;
respdata[i-moveDown].iHP = varPlr->HP;
respdata[i-moveDown].iMaxHP = PC_MAXHEALTH(varPlr->level);
// respdata[i-moveDown]].iMapType = 0;
// respdata[i-moveDown]].iMapNum = 0;
respdata[i-moveDown].iX = varPlr->x;
respdata[i-moveDown].iY = varPlr->y;
respdata[i-moveDown].iZ = varPlr->z;
// client doesnt read nano data here
if (varPlr == plr) {
moveDown = 1;
otherPlr->groupIDs[i] = 0;
} else { // remove the leaving member's buffs from the group and remove the group buffs from the leaving member.
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 2, 1, 0);
if (Nanos::SkillTable[plr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 2, 1, bitFlag);
}
}
plr->iIDGroup = plr->iID;
otherPlr->groupCnt -= 1;
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_LEAVE, resplen);
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1);
sock->sendPacket((void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
}
int Groups::getGroupFlags(Player* plr) {
int bitFlag = 0;
for (int i = 0; i < plr->groupCnt; i++) {
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
if (otherPlr == nullptr)
continue;
bitFlag |= otherPlr->iGroupConditionBitFlag;
}
return bitFlag;
}
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);
}

View File

@@ -1,731 +0,0 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "ItemManager.hpp"
#include "PlayerManager.hpp"
#include "Player.hpp"
#include <string.h> // for memset() and memcmp()
#include <assert.h>
void ItemManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_ITEM, itemGMGiveHandler);
//Trade handlers
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, itemTradeOfferHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT, itemTradeOfferAcceptHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, itemTradeOfferRefusalHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM, itemTradeConfirmHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, itemTradeConfirmCancelHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, itemTradeRegisterItemHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER, itemTradeUnregisterItemHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CASH_REGISTER, itemTradeRegisterCashHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_EMOTES_CHAT, itemTradeChatHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_CHEST_OPEN, chestOpenHandler);
}
void ItemManager::itemMoveHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_ITEM_MOVE))
return; // ignore the malformed packet
sP_CL2FE_REQ_ITEM_MOVE* itemmove = (sP_CL2FE_REQ_ITEM_MOVE*)data->buf;
INITSTRUCT(sP_FE2CL_PC_ITEM_MOVE_SUCC, resp);
PlayerView& plr = PlayerManager::players[sock];
if (plr.plr->Equip[itemmove->iFromSlotNum].iType != 0 && itemmove->eFrom == 0 && itemmove->eTo == 0) {
// this packet should never happen unless it is a weapon, tell the client to do nothing and do nothing ourself
resp.eTo = itemmove->eFrom;
resp.iToSlotNum = itemmove->iFromSlotNum;
resp.ToSlotItem = plr.plr->Equip[itemmove->iToSlotNum];
resp.eFrom = itemmove->eTo;
resp.iFromSlotNum = itemmove->iToSlotNum;
resp.FromSlotItem = plr.plr->Equip[itemmove->iFromSlotNum];
sock->sendPacket((void*)&resp, P_FE2CL_PC_ITEM_MOVE_SUCC, sizeof(sP_FE2CL_PC_ITEM_MOVE_SUCC));
return;
}
if (itemmove->iToSlotNum > AINVEN_COUNT || itemmove->iToSlotNum < 0)
return; // sanity checks
sItemBase fromItem;
sItemBase toItem;
// eFrom 0 means from equip
if (itemmove->eFrom == 0) {
// unequiping an item
fromItem = plr.plr->Equip[itemmove->iFromSlotNum];
} else {
fromItem = plr.plr->Inven[itemmove->iFromSlotNum];
}
// eTo 0 means to equip
if (itemmove->eTo == 0) {
// equiping an item
toItem = plr.plr->Equip[itemmove->iToSlotNum];
plr.plr->Equip[itemmove->iToSlotNum] = fromItem;
} else {
toItem = plr.plr->Inven[itemmove->iToSlotNum];
plr.plr->Inven[itemmove->iToSlotNum] = fromItem;
}
if (itemmove->eFrom == 0) {
plr.plr->Equip[itemmove->iFromSlotNum] = toItem;
} else {
plr.plr->Inven[itemmove->iFromSlotNum] = toItem;
}
if (itemmove->eFrom == 0 || itemmove->eTo == 0) {
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, equipChange);
equipChange.iPC_ID = plr.plr->iID;
if (itemmove->eFrom == 0) {
equipChange.iEquipSlotNum = itemmove->iFromSlotNum;
equipChange.EquipSlotItem = toItem;
} else {
equipChange.iEquipSlotNum = itemmove->iToSlotNum;
equipChange.EquipSlotItem = fromItem;
}
// unequip vehicle if equip slot 8 is 0
if (plr.plr->Equip[8].iID == 0)
plr.plr->iPCState = 0;
// send equip event to other players
for (CNSocket* otherSock : plr.viewable) {
otherSock->sendPacket((void*)&equipChange, P_FE2CL_PC_EQUIP_CHANGE, sizeof(sP_FE2CL_PC_EQUIP_CHANGE));
}
}
resp.eTo = itemmove->eFrom;
resp.iToSlotNum = itemmove->iFromSlotNum;
resp.ToSlotItem = toItem;
resp.eFrom = itemmove->eTo;
resp.iFromSlotNum = itemmove->iToSlotNum;
resp.FromSlotItem = fromItem;
sock->sendPacket((void*)&resp, P_FE2CL_PC_ITEM_MOVE_SUCC, sizeof(sP_FE2CL_PC_ITEM_MOVE_SUCC));
}
void ItemManager::itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_ITEM_DELETE))
return; // ignore the malformed packet
sP_CL2FE_REQ_PC_ITEM_DELETE* itemdel = (sP_CL2FE_REQ_PC_ITEM_DELETE*)data->buf;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_DELETE_SUCC, resp);
PlayerView& plr = PlayerManager::players[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.plr->Inven[itemdel->iSlotNum].iID = 0;
plr.plr->Inven[itemdel->iSlotNum].iType = 0;
plr.plr->Inven[itemdel->iSlotNum].iOpt = 0;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_ITEM_DELETE_SUCC, sizeof(sP_FE2CL_REP_PC_ITEM_DELETE_SUCC));
}
void ItemManager::itemGMGiveHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_GIVE_ITEM))
return; // ignore the malformed packet
sP_CL2FE_REQ_PC_GIVE_ITEM* itemreq = (sP_CL2FE_REQ_PC_GIVE_ITEM*)data->buf;
PlayerView& plr = PlayerManager::players[sock];
// Commented and disabled for future use
//if (!plr.plr->IsGM) {
// TODO: send fail packet
// return;
//}
if (itemreq->eIL == 2) {
// Quest item, not a real item, handle this later, stubbed for now
// sock->sendPacket(new CNPacketData((void*)resp, P_FE2CL_REP_PC_GIVE_ITEM_FAIL, sizeof(sP_FE2CL_REP_PC_GIVE_ITEM_FAIL), sock->getFEKey()));
} else if (itemreq->eIL == 1 && itemreq->Item.iType >= 0 && itemreq->Item.iType <= 10) {
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp);
resp.eIL = itemreq->eIL;
resp.iSlotNum = itemreq->iSlotNum;
resp.Item = itemreq->Item;
plr.plr->Inven[itemreq->iSlotNum] = itemreq->Item;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC, sizeof(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC));
}
}
void ItemManager::itemTradeOfferHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TRADE_OFFER))
return; // ignore the malformed packet
sP_CL2FE_REQ_PC_TRADE_OFFER* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER*)data->buf;
int iID_Check;
if (pacdat->iID_Request == pacdat->iID_From) {
iID_Check = pacdat->iID_To;
} else {
iID_Check = pacdat->iID_From;
}
CNSocket* otherSock = sock;
for (auto pair : PlayerManager::players) {
if (pair.second.plr->iID == iID_Check) {
otherSock = pair.first;
}
}
PlayerView& plr = PlayerManager::players[otherSock];
if (plr.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));
}
void ItemManager::itemTradeOfferAcceptHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT))
return; // ignore the malformed packet
sP_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT*)data->buf;
int iID_Check;
if (pacdat->iID_Request == pacdat->iID_From) {
iID_Check = pacdat->iID_To;
} else {
iID_Check = pacdat->iID_From;
}
CNSocket* otherSock = sock;
for (auto pair : PlayerManager::players) {
if (pair.second.plr->iID == iID_Check) {
otherSock = pair.first;
}
}
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;
// Clearing up trade slots
PlayerView& plr = PlayerManager::players[sock];
PlayerView& plr2 = PlayerManager::players[otherSock];
if (plr2.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
}
plr.plr->isTrading = true;
plr2.plr->isTrading = true;
plr.plr->isTradeConfirm = false;
plr2.plr->isTradeConfirm = false;
for (int i = 0; i < 5; i++) {
plr.plr->Trade[i].iID = 0;
plr.plr->Trade[i].iType = 0;
plr.plr->Trade[i].iOpt = 0;
plr.plr->Trade[i].iInvenNum = 0;
plr.plr->Trade[i].iSlotNum = 0;
}
for (int i = 0; i < 5; i++) {
plr2.plr->Trade[i].iID = 0;
plr2.plr->Trade[i].iType = 0;
plr2.plr->Trade[i].iOpt = 0;
plr2.plr->Trade[i].iInvenNum = 0;
plr2.plr->Trade[i].iSlotNum = 0;
}
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_SUCC));
}
void ItemManager::itemTradeOfferRefusalHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL))
return; // ignore the malformed packet
sP_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL*)data->buf;
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;
int iID_Check;
if (pacdat->iID_Request == pacdat->iID_From) {
iID_Check = pacdat->iID_To;
} else {
iID_Check = pacdat->iID_From;
}
CNSocket* otherSock = sock;
for (auto pair : PlayerManager::players) {
if (pair.second.plr->iID == iID_Check) {
otherSock = pair.first;
}
}
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
}
void ItemManager::itemTradeConfirmHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TRADE_CONFIRM))
return; // ignore the malformed packet
sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf;
int iID_Check;
if (pacdat->iID_Request == pacdat->iID_From) {
iID_Check = pacdat->iID_To;
} else {
iID_Check = pacdat->iID_From;
}
CNSocket* otherSock = sock;
for (auto pair : PlayerManager::players) {
if (pair.second.plr->iID == iID_Check) {
otherSock = pair.first;
}
}
PlayerView& plr = PlayerManager::players[sock];
PlayerView& plr2 = PlayerManager::players[otherSock];
if (plr2.plr->isTradeConfirm) {
plr.plr->isTrading = false;
plr2.plr->isTrading = false;
plr.plr->isTradeConfirm = false;
plr2.plr->isTradeConfirm = false;
// Check if we have enough free slots
int freeSlots = 0;
int freeSlotsNeeded = 0;
int freeSlots2 = 0;
int freeSlotsNeeded2 = 0;
for (int i = 0; i < AINVEN_COUNT; i++) {
if (plr.plr->Inven[i].iID == 0)
freeSlots++;
}
for (int i = 0; i < 5; i++) {
if (plr.plr->Trade[i].iID != 0)
freeSlotsNeeded++;
}
for (int i = 0; i < AINVEN_COUNT; i++) {
if (plr2.plr->Inven[i].iID == 0)
freeSlots2++;
}
for (int i = 0; i < 5; i++) {
if (plr2.plr->Trade[i].iID != 0)
freeSlotsNeeded2++;
}
if (freeSlotsNeeded2 - freeSlotsNeeded > freeSlots) {
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, 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_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
return; // Fail trade because of the lack of slots
}
if (freeSlotsNeeded - freeSlotsNeeded2 > freeSlots2) {
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;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL));
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL));
return; // Fail trade because of the lack of slots
}
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));
// ^^ this is a must have or else the player won't accept a succ packet for some reason
for (int i = 0; i < freeSlotsNeeded; i++) {
plr.plr->Inven[plr.plr->Trade[i].iInvenNum].iID = 0;
plr.plr->Inven[plr.plr->Trade[i].iInvenNum].iType = 0;
plr.plr->Inven[plr.plr->Trade[i].iInvenNum].iOpt = 0;
}
for (int i = 0; i < freeSlotsNeeded2; i++) {
plr2.plr->Inven[plr2.plr->Trade[i].iInvenNum].iID = 0;
plr2.plr->Inven[plr2.plr->Trade[i].iInvenNum].iType = 0;
plr2.plr->Inven[plr2.plr->Trade[i].iInvenNum].iOpt = 0;
}
for (int i = 0; i < AINVEN_COUNT; i++) {
if (freeSlotsNeeded <= 0)
break;
if (plr2.plr->Inven[i].iID == 0) {
plr2.plr->Inven[i].iID = plr.plr->Trade[freeSlotsNeeded - 1].iID;
plr2.plr->Inven[i].iType = plr.plr->Trade[freeSlotsNeeded - 1].iType;
plr2.plr->Inven[i].iOpt = plr.plr->Trade[freeSlotsNeeded - 1].iOpt;
plr.plr->Trade[freeSlotsNeeded - 1].iInvenNum = i;
freeSlotsNeeded--;
}
}
for (int i = 0; i < AINVEN_COUNT; i++) {
if (freeSlotsNeeded2 <= 0)
break;
if (plr.plr->Inven[i].iID == 0) {
plr.plr->Inven[i].iID = plr2.plr->Trade[freeSlotsNeeded2 - 1].iID;
plr.plr->Inven[i].iType = plr2.plr->Trade[freeSlotsNeeded2 - 1].iType;
plr.plr->Inven[i].iOpt = plr2.plr->Trade[freeSlotsNeeded2 - 1].iOpt;
plr2.plr->Trade[freeSlotsNeeded2 - 1].iInvenNum = i;
freeSlotsNeeded2--;
}
}
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.plr->money = plr.plr->money + plr2.plr->moneyInTrade - plr.plr->moneyInTrade;
resp2.iCandy = plr.plr->money;
memcpy(resp2.Item, plr2.plr->Trade, sizeof(plr2.plr->Trade));
memcpy(resp2.ItemStay, plr.plr->Trade, sizeof(plr.plr->Trade));
sock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
plr2.plr->money = plr2.plr->money + plr.plr->moneyInTrade - plr2.plr->moneyInTrade;
resp2.iCandy = plr2.plr->money;
memcpy(resp2.Item, plr.plr->Trade, sizeof(plr.plr->Trade));
memcpy(resp2.ItemStay, plr2.plr->Trade, sizeof(plr2.plr->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, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
plr.plr->isTradeConfirm = true;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM));
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM));
}
}
void ItemManager::itemTradeConfirmCancelHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL))
return; // ignore the malformed packet
sP_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL*)data->buf;
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;
int iID_Check;
if (pacdat->iID_Request == pacdat->iID_From) {
iID_Check = pacdat->iID_To;
} else {
iID_Check = pacdat->iID_From;
}
CNSocket* otherSock = sock;
for (auto pair : PlayerManager::players) {
if (pair.second.plr->iID == iID_Check) {
otherSock = pair.first;
}
}
PlayerView& plr = PlayerManager::players[sock];
PlayerView& plr2 = PlayerManager::players[otherSock];
plr.plr->isTrading = false;
plr.plr->isTradeConfirm = false;
plr2.plr->isTrading = false;
plr2.plr->isTradeConfirm = false;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL));
}
void ItemManager::itemTradeRegisterItemHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TRADE_ITEM_REGISTER))
return; // ignore the malformed packet
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
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;
int iID_Check;
if (pacdat->iID_Request == pacdat->iID_From) {
iID_Check = pacdat->iID_To;
} else {
iID_Check = pacdat->iID_From;
}
CNSocket* otherSock = sock;
for (auto pair : PlayerManager::players) {
if (pair.second.plr->iID == iID_Check) {
otherSock = pair.first;
}
}
PlayerView& plr = PlayerManager::players[sock];
plr.plr->Trade[pacdat->Item.iSlotNum] = pacdat->Item;
plr.plr->isTradeConfirm = false;
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));
}
void ItemManager::itemTradeUnregisterItemHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER))
return; // ignore the malformed packet
sP_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER*)data->buf;
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;
PlayerView& plr = PlayerManager::players[sock];
resp.InvenItem = plr.plr->Trade[pacdat->Item.iSlotNum];
plr.plr->isTradeConfirm = false;
int iID_Check;
if (pacdat->iID_Request == pacdat->iID_From) {
iID_Check = pacdat->iID_To;
} else {
iID_Check = pacdat->iID_From;
}
CNSocket* otherSock = sock;
for (auto pair : PlayerManager::players) {
if (pair.second.plr->iID == iID_Check) {
otherSock = pair.first;
}
}
int temp_num = pacdat->Item.iSlotNum;
if (temp_num >= 0 && temp_num <= 4) {
plr.plr->Trade[temp_num].iID = 0;
plr.plr->Trade[temp_num].iType = 0;
plr.plr->Trade[temp_num].iOpt = 0;
plr.plr->Trade[temp_num].iInvenNum = 0;
plr.plr->Trade[temp_num].iSlotNum = 0;
}
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));
}
void ItemManager::itemTradeRegisterCashHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TRADE_CASH_REGISTER))
return; // ignore the malformed packet
sP_CL2FE_REQ_PC_TRADE_CASH_REGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_CASH_REGISTER*)data->buf;
PlayerView& plr = PlayerManager::players[sock];
if (pacdat->iCandy < 0 || pacdat->iCandy > plr.plr->money)
return; // famous glitch, begone
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;
int iID_Check;
if (pacdat->iID_Request == pacdat->iID_From) {
iID_Check = pacdat->iID_To;
} else {
iID_Check = pacdat->iID_From;
}
CNSocket* otherSock = sock;
for (auto pair : PlayerManager::players) {
if (pair.second.plr->iID == iID_Check) {
otherSock = pair.first;
}
}
plr.plr->moneyInTrade = pacdat->iCandy;
plr.plr->isTradeConfirm = false;
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));
}
void ItemManager::itemTradeChatHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TRADE_EMOTES_CHAT))
return; // malformed packet
sP_CL2FE_REQ_PC_TRADE_EMOTES_CHAT* pacdat = (sP_CL2FE_REQ_PC_TRADE_EMOTES_CHAT*)data->buf;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_EMOTES_CHAT, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
memcpy(resp.szFreeChat, pacdat->szFreeChat, sizeof(pacdat->szFreeChat));
resp.iEmoteCode = pacdat->iEmoteCode;
int iID_Check;
if (pacdat->iID_Request == pacdat->iID_From) {
iID_Check = pacdat->iID_To;
} else {
iID_Check = pacdat->iID_From;
}
CNSocket* otherSock = sock;
for (auto pair : PlayerManager::players) {
if (pair.second.plr->iID == iID_Check) {
otherSock = pair.first;
}
}
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_EMOTES_CHAT, sizeof(sP_FE2CL_REP_PC_TRADE_EMOTES_CHAT));
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_EMOTES_CHAT, sizeof(sP_FE2CL_REP_PC_TRADE_EMOTES_CHAT));
}
void ItemManager::chestOpenHandler(CNSocket *sock, CNPacketData *data) {
if (data->size != sizeof(sP_CL2FE_REQ_ITEM_CHEST_OPEN))
return; // ignore the malformed packet
sP_CL2FE_REQ_ITEM_CHEST_OPEN *pkt = (sP_CL2FE_REQ_ITEM_CHEST_OPEN *)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
// item giving packet
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
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);
// simple rewards
reward->iFatigue = 100; // prevents warning message
reward->iFatigue_Level = 1;
reward->iItemCnt = 1; // remember to update resplen if you change this
// item reward
item->sItem.iType = 0;
item->sItem.iID = 96;
item->iSlotNum = pkt->iSlotNum;
item->eIL = pkt->eIL;
// update player
plr->Inven[pkt->iSlotNum] = item->sItem;
// transmit item
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
// chest opening acknowledgement packet
INITSTRUCT(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, resp);
resp.iSlotNum = pkt->iSlotNum;
std::cout << "opening chest..." << std::endl;
sock->sendPacket((void*)&resp, P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, sizeof(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC));
}
// TODO: use this in cleaned up ItemManager
int ItemManager::findFreeSlot(Player *plr) {
int i;
sItemBase free;
memset((void*)&free, 0, sizeof(sItemBase));
for (i = 0; i < AINVEN_COUNT; i++)
if (memcmp((void*)&plr->Inven[i], (void*)&free, sizeof(sItemBase)) == 0)
return i;
// not found
return -1;
}

View File

@@ -1,25 +0,0 @@
#pragma once
#include "CNShardServer.hpp"
#include "Player.hpp"
namespace ItemManager {
void init();
void itemMoveHandler(CNSocket* sock, CNPacketData* data);
void itemDeleteHandler(CNSocket* sock, CNPacketData* data);
void itemGMGiveHandler(CNSocket* sock, CNPacketData* data);
void itemTradeOfferHandler(CNSocket* sock, CNPacketData* data);
//void itemTradeOfferCancel(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);
int findFreeSlot(Player *plr);
}

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,55 +0,0 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "MissionManager.hpp"
#include "PlayerManager.hpp"
void MissionManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_START, acceptMission);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_END, completeMission);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID, setMission);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_STOP, quitMission);
}
void MissionManager::acceptMission(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);
response.iTaskNum = missionData->iTaskNum;
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
}
void MissionManager::completeMission(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;
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, response);
response.iTaskNum = missionData->iTaskNum;
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_END_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_END_SUCC));
}
void MissionManager::setMission(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID))
return; // malformed packet
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;
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;
INITSTRUCT(sP_FE2CL_REP_PC_TASK_STOP_SUCC, response);
response.iTaskNum = missionData->iTaskNum;
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_STOP_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_STOP_SUCC));
}

View File

@@ -1,12 +0,0 @@
#pragma once
#include "CNShardServer.hpp"
namespace MissionManager {
void init();
void acceptMission(CNSocket* sock, CNPacketData* data);
void completeMission(CNSocket* sock, CNPacketData* data);
void setMission(CNSocket* sock, CNPacketData* data);
void quitMission(CNSocket* sock, CNPacketData* data);
}

659
src/Missions.cpp Normal file
View File

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

55
src/Missions.hpp Normal file
View File

@@ -0,0 +1,55 @@
#pragma once
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "JSON.hpp"
struct Reward {
int32_t id;
int32_t itemTypes[4];
int32_t itemIds[4];
int32_t money;
int32_t fusionmatter;
Reward(int32_t id, nlohmann::json types, nlohmann::json ids, int32_t m, int32_t fm) :
id(id), money(m), fusionmatter(fm) {
for (int i = 0; i < 4; i++) {
itemTypes[i] = types[i];
itemIds[i] = ids[i];
}
};
};
struct TaskData {
/*
* TODO: We'll probably want to keep only the data the server actually needs,
* but for now RE/development is much easier if we have everything at
* our fingertips.
*/
nlohmann::json task;
TaskData(nlohmann::json t) : task(t) {}
// convenience
auto operator[](std::string s) { return task[s]; }
};
namespace Missions {
extern std::map<int32_t, Reward*> Rewards;
extern std::map<int32_t, TaskData*> Tasks;
extern nlohmann::json AvatarGrowth[37];
void init();
int findQSlot(Player *plr, int id);
bool startTask(Player* plr, int TaskID);
// checks if player doesn't have n/n quest items
void updateFusionMatter(CNSocket* sock, int fusion);
void mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls);
void quitTask(CNSocket* sock, int32_t taskNum, bool manual);
void failInstancedMissions(CNSocket* sock);
}

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

View File

@@ -1,37 +1,4 @@
#pragma once
#include "CNStructs.hpp"
class BaseNPC {
public:
sNPCAppearanceData appearanceData;
BaseNPC() {};
BaseNPC(int x, int y, int z, int type) {
appearanceData.iX = x;
appearanceData.iY = y;
appearanceData.iZ = z;
appearanceData.iNPCType = type;
appearanceData.iHP = 400;
appearanceData.iAngle = 0;
appearanceData.iConditionBitFlag = 0;
appearanceData.iBarkerType = 0;
// hopefully no collisions happen :eyes:
appearanceData.iNPC_ID = (int32_t)rand();
};
BaseNPC(int x, int y, int z, int type, int hp, int cond, int angle, int barker) {
appearanceData.iX = x;
appearanceData.iY = y;
appearanceData.iZ = z;
appearanceData.iNPCType = type;
appearanceData.iHP = hp;
appearanceData.iAngle = angle;
appearanceData.iConditionBitFlag = cond;
appearanceData.iBarkerType = barker;
// hopefully no collisions happen :eyes:
appearanceData.iNPC_ID = (int32_t)rand();
}
};
#include "Chunking.hpp"
#include "Entities.hpp"

View File

@@ -1,193 +1,377 @@
#include "NPCManager.hpp"
#include "Items.hpp"
#include "settings.hpp"
#include "Combat.hpp"
#include "Missions.hpp"
#include "Chunking.hpp"
#include "Nanos.hpp"
#include "TableData.hpp"
#include "Groups.hpp"
#include "Racing.hpp"
#include "Vendors.hpp"
#include "Abilities.hpp"
#include "Rand.hpp"
#include <cmath>
#include <algorithm>
#include <list>
#include <fstream>
#include <vector>
#include <assert.h>
#include <limits.h>
#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;
nlohmann::json NPCManager::NPCData;
void NPCManager::init() {
// load NPCs from NPCs.json into our NPC manager
// Temporary fix, IDs will be pulled from json later
int i = 0;
try {
std::ifstream inFile(settings::NPCJSON);
nlohmann::json npcData;
static std::queue<int32_t> RemovalQueue;
// read file into json
inFile >> npcData;
/*
* Initialized at the end of TableData::init().
* This allows us to summon and kill mobs in arbitrary order without
* NPC ID collisions.
*/
int32_t NPCManager::nextId;
for (nlohmann::json::iterator npc = npcData.begin(); npc != npcData.end(); npc++) {
BaseNPC tmp(npc.value()["x"], npc.value()["y"], npc.value()["z"], npc.value()["id"]);
// Temporary fix, IDs will be pulled from json later
tmp.appearanceData.iNPC_ID = i;
i++;
NPCs[tmp.appearanceData.iNPC_ID] = tmp;
if (npc.value()["id"] == 641 || npc.value()["id"] == 642)
RespawnPoints.push_back({ npc.value()["x"], npc.value()["y"], ((int)npc.value()["z"]) + RESURRECT_HEIGHT });
}
}
catch (const std::exception& err) {
std::cerr << "[WARN] Malformed NPCs.json file! Reason:" << err.what() << std::endl;
}
// load temporary mob dump
try {
std::ifstream inFile(settings::MOBJSON); // not in settings, since it's temp
nlohmann::json npcData;
// read file into json
inFile >> npcData;
for (nlohmann::json::iterator npc = npcData.begin(); npc != npcData.end(); npc++) {
BaseNPC tmp(npc.value()["iX"], npc.value()["iY"], npc.value()["iZ"], npc.value()["iNPCType"],
npc.value()["iHP"], npc.value()["iConditionBitFlag"], npc.value()["iAngle"], npc.value()["iBarkerType"]);
// Temporary fix, IDs will be pulled from json later
tmp.appearanceData.iNPC_ID = i;
i++;
NPCs[tmp.appearanceData.iNPC_ID] = tmp;
}
std::cout << "[INFO] populated " << NPCs.size() << " NPCs" << std::endl;
}
catch (const std::exception& err) {
std::cerr << "[WARN] Malformed mobs.json file! Reason:" << err.what() << std::endl;
}
try {
std::ifstream infile(settings::WARPJSON);
nlohmann::json warpData;
// read file into json
infile >> warpData;
for (nlohmann::json::iterator warp = warpData.begin(); warp != warpData.end(); warp++) {
WarpLocation warpLoc = { warp.value()["m_iToX"], warp.value()["m_iToY"], warp.value()["m_iToZ"] };
int warpID = atoi(warp.key().c_str());
Warps[warpID] = warpLoc;
}
std::cout << "[INFO] populated " << Warps.size() << " Warps" << std::endl;
}
catch (const std::exception& err) {
std::cerr << "[WARN] Malformed warps.json file! Reason:" << err.what() << std::endl;
}
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_USE_NPC, npcWarpHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_SUMMON, npcSummonHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler);
}
void NPCManager::updatePlayerNPCS(CNSocket* sock, PlayerView& view) {
std::list<int32_t> yesView;
std::list<int32_t> noView;
for (auto& pair : NPCs) {
int diffX = abs(view.plr->x - pair.second.appearanceData.iX);
int diffY = abs(view.plr->y - pair.second.appearanceData.iY);
if (diffX < settings::NPCDISTANCE && diffY < settings::NPCDISTANCE) {
yesView.push_back(pair.first);
}
else {
noView.push_back(pair.first);
}
}
INITSTRUCT(sP_FE2CL_NPC_EXIT, exitData);
std::list<int32_t>::iterator i = view.viewableNPCs.begin();
while (i != view.viewableNPCs.end()) {
int32_t id = *i;
if (std::find(noView.begin(), noView.end(), id) != noView.end()) {
// it shouldn't be visible, send NPC_EXIT
exitData.iNPC_ID = id;
sock->sendPacket((void*)&exitData, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
// remove from view
view.viewableNPCs.erase(i++);
}
else {
i++;
}
}
INITSTRUCT(sP_FE2CL_NPC_ENTER, enterData);
for (int32_t id : yesView) {
if (std::find(view.viewableNPCs.begin(), view.viewableNPCs.end(), id) == view.viewableNPCs.end()) {
// needs to be added to viewableNPCs! send NPC_ENTER
enterData.NPCAppearanceData = NPCs[id].appearanceData;
sock->sendPacket((void*)&enterData, P_FE2CL_NPC_ENTER, sizeof(sP_FE2CL_NPC_ENTER));
// add to viewable
view.viewableNPCs.push_back(id);
}
}
PlayerManager::players[sock].viewableNPCs = view.viewableNPCs;
}
void NPCManager::npcBarkHandler(CNSocket* sock, CNPacketData* data) {} // stubbed for now
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;
INITSTRUCT(sP_FE2CL_NPC_ENTER, resp);
Player* plr = PlayerManager::getPlayer(sock);
// permission & sanity check
if (!plr->IsGM || req->iNPCType >= 3314)
void NPCManager::destroyNPC(int32_t id) {
// sanity check
if (NPCs.find(id) == NPCs.end()) {
std::cout << "npc not found: " << id << std::endl;
return;
}
resp.NPCAppearanceData.iNPC_ID = rand(); // cpunch-style
resp.NPCAppearanceData.iNPCType = req->iNPCType;
resp.NPCAppearanceData.iHP = 1000; // TODO: placeholder
resp.NPCAppearanceData.iX = plr->x;
resp.NPCAppearanceData.iY = plr->y;
resp.NPCAppearanceData.iZ = plr->z;
sock->sendPacket((void*)&resp, P_FE2CL_NPC_ENTER, sizeof(sP_FE2CL_NPC_ENTER));
}
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;
PlayerView& plrv = PlayerManager::players[sock];
BaseNPC* entity = NPCs[id];
// sanity check
if (Warps.find(warpNpc->iWarpID) == Warps.end())
if (!Chunking::chunkExists(entity->chunkPos)) {
std::cout << "chunk not found!" << std::endl;
return;
}
// remove NPC from the chunk
EntityRef ref = {id};
Chunking::untrackEntity(entity->chunkPos, ref);
// remove from viewable chunks
Chunking::removeEntityFromChunks(Chunking::getViewableChunks(entity->chunkPos), ref);
// finally, remove it from the map and free it
NPCs.erase(id);
delete entity;
}
void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I, int angle) {
BaseNPC* npc = NPCs[id];
npc->appearanceData.iAngle = angle;
ChunkPos oldChunk = npc->chunkPos;
ChunkPos newChunk = Chunking::chunkPosAt(X, Y, I);
npc->x = X;
npc->y = Y;
npc->z = Z;
npc->instanceID = I;
if (oldChunk == newChunk)
return; // didn't change chunks
Chunking::updateEntityChunk({id}, oldChunk, newChunk);
}
void NPCManager::sendToViewable(BaseNPC *npc, void *buf, uint32_t type, size_t size) {
for (auto it = npc->viewableChunks.begin(); it != npc->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
if (ref.type == EntityType::PLAYER)
ref.sock->sendPacket(buf, type, size);
}
}
}
static void npcBarkHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf;
// get bark IDs from task data
TaskData* td = Missions::Tasks[req->iMissionTaskID];
std::vector<int> barks;
for (int i = 0; i < 4; i++) {
if (td->task["m_iHBarkerTextID"][i] != 0) // non-zeroes only
barks.push_back(td->task["m_iHBarkerTextID"][i]);
}
if (barks.empty())
return; // no barks
INITSTRUCT(sP_FE2CL_REP_BARKER, resp);
resp.iNPC_ID = req->iNPC_ID;
resp.iMissionStringID = barks[Rand::rand(barks.size())];
sock->sendPacket(resp, P_FE2CL_REP_BARKER);
}
static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
if (plr->accountLevel > 30)
return;
// send to client
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_NPC_SUCC, resp);
resp.iX = Warps[warpNpc->iWarpID].x;
resp.iY = Warps[warpNpc->iWarpID].y;
resp.iZ = Warps[warpNpc->iWarpID].z;
sP_CL2FE_REQ_NPC_UNSUMMON* req = (sP_CL2FE_REQ_NPC_UNSUMMON*)data->buf;
NPCManager::destroyNPC(req->iNPC_ID);
}
// force player & NPC reload
plrv.viewable.clear();
plrv.viewableNPCs.clear();
// type must already be checked and updateNPCPosition() must be called on the result
BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) {
uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
#define EXTRA_HEIGHT 0
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC, sizeof(sP_FE2CL_REP_PC_WARP_USE_NPC_SUCC));
}
//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);
// re-enable respawning, if desired
((Mob*)npc)->summoned = !respawn;
} else
npc = new BaseNPC(x, y, z + EXTRA_HEIGHT, 0, inst, type, id);
NPCs[id] = npc;
return npc;
}
static void npcSummonHandler(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_NPC_SUMMON*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
int limit = NPCData.back()["m_iNpcNumber"];
// permission & sanity check
if (plr->accountLevel > 30 || req->iNPCType > limit || req->iNPCCnt > 100)
return;
for (int i = 0; i < req->iNPCCnt; i++) {
BaseNPC *npc = summonNPC(plr->x, plr->y, plr->z, plr->instanceID, req->iNPCType);
updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, 0);
}
}
static void handleWarp(CNSocket* sock, int32_t warpId) {
Player* plr = PlayerManager::getPlayer(sock);
// sanity check
if (Warps.find(warpId) == Warps.end())
return;
if (plr->iPCState & 8) {
// remove the player's vehicle
plr->iPCState &= ~8;
// send to self
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, off);
sock->sendPacket(off, P_FE2CL_PC_VEHICLE_OFF_SUCC);
// send to others
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, chg);
chg.iPC_ID = plr->iID;
chg.iState = plr->iPCState;
PlayerManager::sendToViewable(sock, chg, P_FE2CL_PC_STATE_CHANGE);
}
// std::cerr << "Warped to Map Num:" << Warps[warpId].instanceID << " NPC ID " << Warps[warpId].npcID << std::endl;
if (Warps[warpId].isInstance) {
uint64_t instanceID = Warps[warpId].instanceID;
// 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
Chunking::createInstance(instanceID);
// save Lair entrance coords as a pseudo-Resurrect 'Em
plr->recallX = Warps[warpId].x;
plr->recallY = Warps[warpId].y;
plr->recallZ = Warps[warpId].z + RESURRECT_HEIGHT;
plr->recallInstance = instanceID;
}
if (plr->iID == plr->iIDGroup && plr->groupCnt == 1)
PlayerManager::sendPlayerTo(sock, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
else {
Player* leaderPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
for (int i = 0; i < leaderPlr->groupCnt; i++) {
Player* otherPlr = PlayerManager::getPlayerFromID(leaderPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(leaderPlr->groupIDs[i]);
if (otherPlr == nullptr || sockTo == nullptr)
continue;
// save Lair entrance coords for everyone else as well
otherPlr->recallX = Warps[warpId].x;
otherPlr->recallY = Warps[warpId].y;
otherPlr->recallZ = Warps[warpId].z + RESURRECT_HEIGHT;
otherPlr->recallInstance = instanceID;
// remove their vehicle if they're on one
if (otherPlr->iPCState & 8) {
// remove the player's vehicle
otherPlr->iPCState &= ~8;
// send to self
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, off);
sockTo->sendPacket(off, P_FE2CL_PC_VEHICLE_OFF_SUCC);
// send to others
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, chg);
chg.iPC_ID = otherPlr->iID;
chg.iState = otherPlr->iPCState;
PlayerManager::sendToViewable(sockTo, chg, P_FE2CL_PC_STATE_CHANGE);
}
PlayerManager::sendPlayerTo(sockTo, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
}
}
}
else
{
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_NPC_SUCC, resp); // Can only be used for exiting instances because it sets the instance flag to false
resp.iX = Warps[warpId].x;
resp.iY = Warps[warpId].y;
resp.iZ = Warps[warpId].z;
resp.iCandy = plr->money;
resp.eIL = 4; // do not take away any items
uint64_t fromInstance = plr->instanceID; // pre-warp instance, saved for post-warp
plr->instanceID = INSTANCE_OVERWORLD;
Missions::failInstancedMissions(sock); // fail any instanced missions
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC);
PlayerManager::updatePlayerPositionForWarp(sock, resp.iX, resp.iY, resp.iZ, INSTANCE_OVERWORLD);
// remove the player's ongoing race, if any
if (Racing::EPRaces.find(sock) != Racing::EPRaces.end())
Racing::EPRaces.erase(sock);
// post-warp: check if the source instance has no more players in it and delete it if so
Chunking::destroyInstanceIfEmpty(fromInstance);
}
}
static void npcWarpHandler(CNSocket* sock, CNPacketData* data) {
auto warpNpc = (sP_CL2FE_REQ_PC_WARP_USE_NPC*)data->buf;
handleWarp(sock, warpNpc->iWarpID);
}
static void npcWarpTimeMachine(CNSocket* sock, CNPacketData* data) {
// this is just a warp request
handleWarp(sock, 28);
}
/*
* Helper function to get NPC closest to coordinates in specified chunks
*/
BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z) {
BaseNPC* npc = nullptr;
int lastDist = INT_MAX;
for (auto c = chunks->begin(); c != chunks->end(); c++) { // haha get it
Chunk* chunk = *c;
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
if (ent->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;
}
}
}
return npc;
}
// TODO: Move this to MobAI, possibly
#pragma region NPCEvents
// summon right arm and stage 2 body
static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc; // adaptium, stun
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage two" << std::endl;
// Fuse doesn't move; spawnX, etc. is shorter to write than *appearanceData*
// Blastons, Heal
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2467);
newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
// right arm, Adaptium, Stun
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX - 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2469);
arm->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
}
// summon left arm and stage 3 body
static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc;
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage three" << std::endl;
// Cosmix, Damage Point
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2468);
newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
// Blastons, Heal
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX + 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2470);
arm->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
}
std::vector<NPCEvent> NPCManager::NPCEvents = {
NPCEvent(2466, ON_KILLED, lordFuseStageTwo),
NPCEvent(2467, ON_KILLED, lordFuseStageThree),
};
#pragma endregion NPCEvents
void NPCManager::queueNPCRemoval(int32_t id) {
RemovalQueue.push(id);
}
static void step(CNServer *serv, time_t currTime) {
for (auto& pair : NPCs) {
if (pair.second->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,28 +1,52 @@
#pragma once
#include "CNProtocol.hpp"
#include "core/Core.hpp"
#include "PlayerManager.hpp"
#include "NPC.hpp"
#include "Transport.hpp"
#include "JSON.hpp"
#include <map>
#include <unordered_map>
#include <vector>
#define RESURRECT_HEIGHT 400
// this should really be called vec3 or something...
struct WarpLocation {
int x, y, z;
enum Trigger {
ON_KILLED,
ON_COMBAT
};
typedef void (*NPCEventHandler)(CNSocket*, BaseNPC*);
struct NPCEvent {
int32_t npcType;
int trigger;
NPCEventHandler handler;
NPCEvent(int32_t t, int tr, NPCEventHandler hndlr)
: npcType(t), trigger(tr), handler(hndlr) {}
};
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 nlohmann::json NPCData;
extern int32_t nextId;
void init();
void npcBarkHandler(CNSocket* sock, CNPacketData* data);
void npcSummonHandler(CNSocket* sock, CNPacketData* data);
void npcWarpHandler(CNSocket* sock, CNPacketData* data);
void queueNPCRemoval(int32_t);
void destroyNPC(int32_t);
void updateNPCPosition(int32_t, int X, int Y, int Z, uint64_t I, int angle);
void updatePlayerNPCS(CNSocket* sock, PlayerView& plr);
void sendToViewable(BaseNPC* npc, void* buf, uint32_t type, size_t size);
BaseNPC *summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn=false, bool baseInstance=false);
BaseNPC* getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z);
}

View File

@@ -1,210 +0,0 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "NanoManager.hpp"
#include "PlayerManager.hpp"
void NanoManager::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_PC_GIVE_NANO, nanoGMGiveHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_TUNE, nanoSkillSetHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_SKILL_USE, nanoSkillUseHandler);
}
void NanoManager::nanoEquipHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_NANO_EQUIP))
return; // malformed packet
sP_CL2FE_REQ_NANO_EQUIP* nano = (sP_CL2FE_REQ_NANO_EQUIP*)data->buf;
INITSTRUCT(sP_FE2CL_REP_NANO_EQUIP_SUCC, resp);
Player *plr = PlayerManager::getPlayer(sock);
// sanity check
if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0)
return;
resp.iNanoID = nano->iNanoID;
resp.iNanoSlotNum = nano->iNanoSlotNum;
// Update player
plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID;
// unsummon nano if replaced
if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum])
summonNano(sock, -1);
sock->sendPacket((void*)&resp, P_FE2CL_REP_NANO_EQUIP_SUCC, sizeof(sP_FE2CL_REP_NANO_EQUIP_SUCC));
}
void NanoManager::nanoUnEquipHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_NANO_UNEQUIP))
return; // malformed packet
sP_CL2FE_REQ_NANO_UNEQUIP* 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((void*)&resp, P_FE2CL_REP_NANO_UNEQUIP_SUCC, sizeof(sP_FE2CL_REP_NANO_UNEQUIP_SUCC));
}
void NanoManager::nanoGMGiveHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_GIVE_NANO))
return; // ignore the malformed packet
// Cmd: /nano <nanoId>
sP_CL2FE_REQ_PC_GIVE_NANO* nano = (sP_CL2FE_REQ_PC_GIVE_NANO*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
// Add nano to player
addNano(sock, nano->iNanoID, 0);
DEBUGLOG(
std::cout << U16toU8(plr->PCStyle.szFirstName) << U16toU8(plr->PCStyle.szLastName) << " requested to add nano id: " << nano->iNanoID << std::endl;
)
}
void NanoManager::nanoSummonHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_NANO_ACTIVE))
return; // malformed packet
sP_CL2FE_REQ_NANO_ACTIVE* pkt = (sP_CL2FE_REQ_NANO_ACTIVE*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
summonNano(sock, pkt->iNanoSlotNum);
// Send to client
DEBUGLOG(
std::cout << U16toU8(plr->PCStyle.szFirstName) << U16toU8(plr->PCStyle.szLastName) << " requested to summon nano slot: " << pkt->iNanoSlotNum << std::endl;
)
}
void NanoManager::nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_NANO_SKILL_USE))
return; // malformed packet
sP_CL2FE_REQ_NANO_SKILL_USE* skill = (sP_CL2FE_REQ_NANO_SKILL_USE*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
// Send to client
INITSTRUCT(sP_FE2CL_NANO_SKILL_USE_SUCC, resp);
resp.iArg1 = skill->iArg1;
resp.iArg2 = skill->iArg2;
resp.iArg3 = skill->iArg3;
resp.iBulletID = skill->iBulletID;
resp.iTargetCnt = skill->iTargetCnt;
resp.iPC_ID = plr->iID;
resp.iNanoStamina = 150; // Hardcoded for now
sock->sendPacket((void*)&resp, P_FE2CL_NANO_SKILL_USE_SUCC, sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC));
DEBUGLOG(
std::cout << U16toU8(plr->PCStyle.szFirstName) << U16toU8(plr->PCStyle.szLastName) << " requested to summon nano skill " << std::endl;
)
}
void NanoManager::nanoSkillSetHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_NANO_TUNE))
return; // malformed packet
sP_CL2FE_REQ_NANO_TUNE* skill = (sP_CL2FE_REQ_NANO_TUNE*)data->buf;
setNanoSkill(sock, skill->iNanoID, skill->iTuneID);
}
#pragma region Helper methods
void NanoManager::addNano(CNSocket* sock, int16_t nanoId, int16_t slot) {
if (nanoId > 36)
return;
Player *plr = PlayerManager::getPlayer(sock);
// Send to client
INITSTRUCT(sP_FE2CL_REP_PC_NANO_CREATE_SUCC, resp);
resp.Nano.iID = nanoId;
resp.Nano.iStamina = 150;
resp.iQuestItemSlotNum = slot;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_NANO_CREATE_SUCC, sizeof(sP_FE2CL_REP_PC_NANO_CREATE_SUCC));
// Update player
plr->Nanos[nanoId] = resp.Nano;
}
void NanoManager::summonNano(CNSocket *sock, int slot) {
INITSTRUCT(sP_FE2CL_REP_NANO_ACTIVE_SUCC, resp);
resp.iActiveNanoSlotNum = slot;
sock->sendPacket((void*)&resp, P_FE2CL_REP_NANO_ACTIVE_SUCC, sizeof(sP_FE2CL_REP_NANO_ACTIVE_SUCC));
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "summon nano\n";
if (slot > 2 || slot < 0)
return; //sanity check
int nanoId = plr->equippedNanos[slot];
if (nanoId > 36 || nanoId < 0)
return; // sanity check
sNano nano = plr->Nanos[nanoId];
// Send to other players
INITSTRUCT(sP_FE2CL_NANO_ACTIVE, pkt1);
pkt1.iPC_ID = plr->iID;
pkt1.Nano = nano;
for (CNSocket* s : PlayerManager::players[sock].viewable)
s->sendPacket((void*)&pkt1, P_FE2CL_NANO_ACTIVE, sizeof(sP_FE2CL_NANO_ACTIVE));
// update player
plr->activeNano = nanoId;
}
void NanoManager::setNanoSkill(CNSocket* sock, int16_t nanoId, int16_t skillId) {
if (nanoId > 36)
return;
Player *plr = PlayerManager::getPlayer(sock);
sNano nano = plr->Nanos[nanoId];
nano.iSkillID = skillId;
plr->Nanos[nanoId] = nano;
// Send to client
INITSTRUCT(sP_FE2CL_REP_NANO_TUNE_SUCC, resp);
resp.iNanoID = nanoId;
resp.iSkillID = skillId;
sock->sendPacket((void*)&resp, P_FE2CL_REP_NANO_TUNE_SUCC, sizeof(sP_FE2CL_REP_NANO_TUNE_SUCC));
DEBUGLOG(
std::cout << U16toU8(plr->PCStyle.szFirstName) << U16toU8(plr->PCStyle.szLastName) << " set skill id " << skillId << " for nano: " << nanoId << std::endl;
)
}
void NanoManager::resetNanoSkill(CNSocket* sock, int16_t nanoId) {
if (nanoId > 36)
return;
Player *plr = PlayerManager::getPlayer(sock);
sNano nano = plr->Nanos[nanoId];
// 0 is reset
nano.iSkillID = 0;
plr->Nanos[nanoId] = nano;
}
#pragma endregion

View File

@@ -1,19 +0,0 @@
#pragma once
#include "CNShardServer.hpp"
namespace NanoManager {
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);
// Helper methods
void addNano(CNSocket* sock, int16_t nanoId, int16_t slot);
void summonNano(CNSocket* sock, int slot);
void setNanoSkill(CNSocket* sock, int16_t nanoId, int16_t skillId);
void resetNanoSkill(CNSocket* sock, int16_t nanoId);
}

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

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

View File

@@ -3,33 +3,90 @@
#include <string>
#include <cstring>
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include "core/Core.hpp"
#include "Chunking.hpp"
#include "Entities.hpp"
struct Player {
int accountId;
int64_t SerialKey;
int32_t iID;
uint64_t FEKey;
#define ACTIVE_MISSION_COUNT 6
int level;
int HP;
int slot; // player slot, not nano slot
int32_t money;
int32_t fusionmatter;
sPCStyle PCStyle;
sPCStyle2 PCStyle2;
sNano Nanos[37]; // acquired nanos
int equippedNanos[3];
int activeNano; // active nano (index into Nanos)
int8_t iPCState;
#define PC_MAXHEALTH(level) (925 + 75 * (level))
int x, y, z, angle;
sItemBase Equip[AEQUIP_COUNT];
sItemBase Inven[AINVEN_COUNT];
sItemTrade Trade[12];
int32_t moneyInTrade;
bool isTrading;
bool isTradeConfirm;
bool IsGM;
struct Player : public Entity {
int accountId = 0;
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
int32_t iID = 0;
int level = 0;
int HP = 0;
int slot = 0; // player slot, not nano slot
int16_t mentor = 0;
int32_t money = 0;
int32_t fusionmatter = 0;
int32_t batteryW = 0;
int32_t batteryN = 0;
sPCStyle PCStyle = {};
sPCStyle2 PCStyle2 = {};
sNano Nanos[NANO_COUNT] = {}; // acquired nanos
int equippedNanos[3] = {};
int activeNano = 0; // active nano (index into Nanos)
int8_t iPCState = 0;
int32_t iWarpLocationFlag = 0;
int64_t aSkywayLocationFlag[2] = {};
int32_t iConditionBitFlag = 0;
int32_t iSelfConditionBitFlag = 0;
int8_t iSpecialState = 0;
int angle = 0;
int lastX = 0, lastY = 0, lastZ = 0, lastAngle = 0;
int recallX = 0, recallY = 0, recallZ = 0, recallInstance = 0; // also Lair entrances
sItemBase Equip[AEQUIP_COUNT] = {};
sItemBase Inven[AINVEN_COUNT] = {};
sItemBase Bank[ABANK_COUNT] = {};
sItemTrade Trade[12] = {};
int32_t moneyInTrade = 0;
bool isTrading = false;
bool isTradeConfirm = false;
bool inCombat = false;
bool onMonkey = false;
int nanoDrainRate = 0;
int healCooldown = 0;
int pointDamage = 0;
int groupDamage = 0;
int fireRate = 0;
int defense = 0;
int64_t aQuestFlag[16] = {};
int tasks[ACTIVE_MISSION_COUNT] = {};
int RemainingNPCCount[ACTIVE_MISSION_COUNT][3] = {};
sItemBase QInven[AQINVEN_COUNT] = {};
int32_t CurrentMissionID = 0;
sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {};
int32_t iIDGroup = 0;
int groupCnt = 0;
int32_t groupIDs[4] = {};
int32_t iGroupConditionBitFlag = 0;
bool notify = false;
bool hidden = false;
bool unwarpable = false;
bool buddiesSynced = false;
int64_t buddyIDs[50] = {};
bool isBuddyBlocked[50] = {};
uint64_t iFirstUseFlag[2] = {};
time_t lastHeartbeat = 0;
int suspicionRating = 0;
time_t lastShot = 0;
std::vector<sItemBase> buyback = {};
Player() { 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,58 +1,53 @@
#pragma once
#include "Player.hpp"
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include "CNShardServer.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "Chunking.hpp"
#include <utility>
#include <map>
#include <list>
struct WarpLocation;
struct PlayerView {
std::list<CNSocket*> viewable;
std::list<int32_t> viewableNPCs;
Player *plr;
uint64_t lastHeartbeat;
};
namespace PlayerManager {
extern std::map<CNSocket*, PlayerView> players;
extern std::map<CNSocket*, Player*> players;
void init();
void addPlayer(CNSocket* key, Player plr);
void removePlayer(CNSocket* key);
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z);
std::list<CNSocket*> getNearbyPlayers(int X, int Y, int dist);
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle);
void updatePlayerPositionForWarp(CNSocket* sock, int X, int Y, int Z, uint64_t inst);
void enterPlayer(CNSocket* sock, CNPacketData* data);
void loadPlayer(CNSocket* sock, CNPacketData* data);
void movePlayer(CNSocket* sock, CNPacketData* data);
void stopPlayer(CNSocket* sock, CNPacketData* data);
void jumpPlayer(CNSocket* sock, CNPacketData* data);
void jumppadPlayer(CNSocket* sock, CNPacketData* data);
void launchPlayer(CNSocket* sock, CNPacketData* data);
void ziplinePlayer(CNSocket* sock, CNPacketData* data);
void movePlatformPlayer(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 changePlayerGuide(CNSocket *sock, CNPacketData *data);
void enterPlayerVehicle(CNSocket* sock, CNPacketData* data);
void exitPlayerVehicle(CNSocket* sock, CNPacketData* data);
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I);
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z);
Player *getPlayer(CNSocket* key);
WarpLocation getRespawnPoint(Player *plr);
std::string getPlayerName(Player *plr, bool id=true);
bool isAccountInUse(int accountId);
void exitDuplicate(int accountId);
Player *getPlayerFromID(int32_t iID);
CNSocket *getSockFromID(int32_t iID);
CNSocket *getSockFromName(std::string firstname, std::string lastname);
CNSocket *getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname);
WarpLocation *getRespawnPoint(Player *plr);
void 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();
};

178
src/Racing.cpp Normal file
View File

@@ -0,0 +1,178 @@
#include "servers/CNShardServer.hpp"
#include "Racing.hpp"
#include "PlayerManager.hpp"
#include "Missions.hpp"
#include "Items.hpp"
#include "db/Database.hpp"
#include "NPCManager.hpp"
using namespace Racing;
std::map<int32_t, EPInfo> Racing::EPData;
std::map<CNSocket*, EPRace> Racing::EPRaces;
std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> Racing::EPRewards;
static void racingStart(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_EP_RACE_START*)data->buf;
if (NPCManager::NPCs.find(req->iStartEcomID) == NPCManager::NPCs.end())
return; // starting line agent not found
int mapNum = MAPNUM(NPCManager::NPCs[req->iStartEcomID]->instanceID);
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
return; // IZ not found
// make ongoing race entry
EPRace race = { {}, req->iEPRaceMode, req->iEPTicketItemSlotNum, getTime() / 1000 };
EPRaces[sock] = race;
INITSTRUCT(sP_FE2CL_REP_EP_RACE_START_SUCC, resp);
resp.iStartTick = 0; // ignored
resp.iLimitTime = EPData[mapNum].maxTime;
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_START_SUCC);
}
static void racingGetPod(CNSocket* sock, CNPacketData* data) {
if (EPRaces.find(sock) == EPRaces.end())
return; // race not found
auto req = (sP_CL2FE_REQ_EP_GET_RING*)data->buf;
if (EPRaces[sock].collectedRings.count(req->iRingLID))
return; // can't collect the same ring twice
// without an anticheat system, we really don't have a choice but to honor the request
// TODO: proximity check so players can't cheat the race by replaying packets
EPRaces[sock].collectedRings.insert(req->iRingLID);
INITSTRUCT(sP_FE2CL_REP_EP_GET_RING_SUCC, resp);
resp.iRingLID = req->iRingLID;
resp.iRingCount_Get = EPRaces[sock].collectedRings.size();
sock->sendPacket(resp, P_FE2CL_REP_EP_GET_RING_SUCC);
}
static void racingCancel(CNSocket* sock, CNPacketData* data) {
if (EPRaces.find(sock) == EPRaces.end())
return; // race not found
Player* plr = PlayerManager::getPlayer(sock);
EPRaces.erase(sock);
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
/*
* This request packet is used for both cancelling the race via the
* NPC at the start, *and* failing the race by running out of time.
* If the latter is to happen, the client disables movement until it
* receives a packet from the server that re-enables it.
*
* So, in order to prevent a potential softlock we respawn the player.
*/
WarpLocation* respawnLoc = PlayerManager::getRespawnPoint(plr);
if (respawnLoc != nullptr) {
PlayerManager::sendPlayerTo(sock, respawnLoc->x, respawnLoc->y, respawnLoc->z, respawnLoc->instanceID);
} else {
// fallback, just respawn the player in-place if no suitable point is found
PlayerManager::sendPlayerTo(sock, plr->x, plr->y, plr->z, plr->instanceID);
}
}
static void racingEnd(CNSocket* sock, CNPacketData* data) {
if (EPRaces.find(sock) == EPRaces.end())
return; // race not found
auto req = (sP_CL2FE_REQ_EP_RACE_END*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
if (NPCManager::NPCs.find(req->iEndEcomID) == NPCManager::NPCs.end())
return; // finish line agent not found
int mapNum = MAPNUM(NPCManager::NPCs[req->iEndEcomID]->instanceID);
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
return; // IZ not found
uint64_t now = getTime() / 1000;
int timeDiff = now - EPRaces[sock].startTime;
int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff;
if (score < 0) score = 0; // lol
int fm = score * plr->level * (1.0f / 36) * 0.3f;
// we submit the ranking first...
Database::RaceRanking postRanking = {};
postRanking.EPID = EPData[mapNum].EPID;
postRanking.PlayerID = plr->iID;
postRanking.RingCount = EPRaces[sock].collectedRings.size();
postRanking.Score = score;
postRanking.Time = timeDiff;
postRanking.Timestamp = getTimestamp();
Database::postRaceRanking(postRanking);
// ...then we get the top ranking, which may or may not be what we just submitted
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(EPData[mapNum].EPID, plr->iID);
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
// get rank scores and rewards
std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first;
std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second;
// top ranking
int topRank = 0;
while (rankScores->at(topRank) > topRankingPlayer.Score)
topRank++;
resp.iEPTopRank = topRank + 1;
resp.iEPTopRingCount = topRankingPlayer.RingCount;
resp.iEPTopScore = topRankingPlayer.Score;
resp.iEPTopTime = topRankingPlayer.Time;
// this ranking
int rank = 0;
while (rankScores->at(rank) > postRanking.Score)
rank++;
resp.iEPRank = rank + 1;
resp.iEPRingCnt = postRanking.RingCount;
resp.iEPScore = postRanking.Score;
resp.iEPRaceTime = postRanking.Time;
resp.iEPRaceMode = EPRaces[sock].mode;
resp.iEPRewardFM = fm;
Missions::updateFusionMatter(sock, resp.iEPRewardFM);
resp.iFusionMatter = plr->fusionmatter;
resp.iFatigue = 50;
resp.iFatigue_Level = 1;
sItemReward reward;
reward.iSlotNum = Items::findFreeSlot(plr);
reward.eIL = 1;
sItemBase item;
item.iID = rankRewards->at(rank); // rank scores and rewards line up
item.iType = 9;
item.iOpt = 1;
item.iTimeLimit = 0;
reward.sItem = item;
if (reward.iSlotNum > -1 && reward.sItem.iID != 0) {
resp.RewardItem = reward;
plr->Inven[reward.iSlotNum] = item;
}
EPRaces.erase(sock);
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_END_SUCC);
}
void Racing::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_START, racingStart);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_GET_RING, racingGetPod);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_CANCEL, racingCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_END, racingEnd);
}

23
src/Racing.hpp Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include <set>
#include "servers/CNShardServer.hpp"
struct EPInfo {
int zoneX, zoneY, EPID, maxScore, maxTime;
};
struct EPRace {
std::set<int> collectedRings;
int mode, ticketSlot;
time_t startTime;
};
namespace Racing {
extern std::map<int32_t, EPInfo> EPData;
extern std::map<CNSocket*, EPRace> EPRaces;
extern std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> EPRewards;
void init();
}

91
src/Rand.cpp Normal file
View File

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

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

1394
src/TableData.cpp Normal file

File diff suppressed because it is too large Load Diff

27
src/TableData.hpp Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <map>
#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<Vec3>> RunningSkywayRoutes;
extern std::map<int32_t, int> RunningNPCRotations;
extern std::map<int32_t, int> RunningNPCMapNumbers;
extern std::unordered_map<int32_t, std::pair<BaseNPC*, std::vector<BaseNPC*>>> RunningNPCPaths; // player ID -> following NPC
extern std::vector<NPCPath> FinishedNPCPaths; // NPC ID -> path
extern std::map<int32_t, BaseNPC*> RunningMobs;
extern std::map<int32_t, BaseNPC*> RunningGroups;
extern std::map<int32_t, BaseNPC*> RunningEggs;
void init();
void flush();
}

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

425
src/Transport.cpp Normal file
View File

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

View File

@@ -1,9 +0,0 @@
#pragma once
#include "CNShardServer.hpp"
namespace TransportManager {
void init();
void transportRegisterLocationHandler(CNSocket* sock, CNPacketData* data);
}

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

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

View File

@@ -5,8 +5,12 @@
#include <iostream>
#include <stdio.h>
#include <stdint.h>
#ifdef _WIN32
#ifdef _WIN32
// windows
#ifndef NOMINMAX
#define NOMINMAX
#endif
#define M_PI 3.14159265358979323846
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <windows.h>
@@ -14,6 +18,8 @@
#pragma comment(lib, "Ws2_32.lib")
typedef char buffer_t;
#define PollFD WSAPOLLFD
#define poll WSAPoll
#define OF_ERRNO WSAGetLastError()
#define OF_EWOULD WSAEWOULDBLOCK
#define SOCKETINVALID(x) (x == INVALID_SOCKET)
@@ -23,11 +29,13 @@
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <unistd.h>
#include <errno.h>
typedef int SOCKET;
typedef void buffer_t;
#define PollFD struct pollfd
#define OF_ERRNO errno
#define OF_EWOULD EWOULDBLOCK
#define SOCKETINVALID(x) (x < 0)
@@ -40,49 +48,61 @@
#include <csignal>
#include <list>
#include <queue>
#include <unordered_map>
#include <vector>
#include <algorithm>
#include "Defines.hpp"
#include "Packets.hpp"
#include "settings.hpp"
#if defined(__MINGW32__) && !defined(_GLIBCXX_HAS_GTHREADS)
#include "mingw/mingw.mutex.h"
#else
#else
#include <mutex>
#endif
/*
Packets format (sent from the client):
[4 bytes] - size of packet including the 4 byte packet type
[size bytes] - Encrypted packet (byte swapped && xor'd with 8 byte key; see CNSocketEncryption)
[4 bytes] - packet type (which is a combination of the first 4 bytes of the packet and a checksum in some versions)
[structure] - one member contains length of trailing data (expressed in packet-dependant structures)
[trailing data] - optional variable-length data that only some packets make use of
*/
* Packets format (sent from the client):
* [4 bytes] - size of packet including the 4 byte packet type
* [size bytes] - Encrypted packet (byte swapped && xor'd with 8 byte key; see CNSocketEncryption)
* [4 bytes] - packet type (which is a combination of the first 4 bytes of the packet and a checksum in some versions)
* [structure] - one member contains length of trailing data (expressed in packet-dependant structures)
* [trailing data] - optional variable-length data that only some packets make use of
*/
// error checking calloc wrapper
inline void* xmalloc(size_t sz) {
void* res = calloc(1, sz);
if (res == NULL) {
std::cerr << "[FATAL] OpenFusion: calloc failed to allocate memory!" << std::endl;
std::cerr << "[FATAL] OpenFusion: out of memory!" << std::endl;
exit(EXIT_FAILURE);
}
return res;
}
inline constexpr bool isInboundPacketID(uint32_t id) {
return ((id & CL2LS) == CL2LS) || ((id & CL2FE) == CL2FE);
}
inline constexpr bool isOutboundPacketID(uint32_t id) {
return ((id & LS2CL) == LS2CL) || ((id & FE2CL) == FE2CL);
}
// overflow-safe validation of variable-length packets
// for outbound packets
inline 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 / (size_t)npayloads < plsize)
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
return false;
// it's safe to multiply
size_t trailing = npayloads * plsize;
// does it fit in a packet?
if (base + trailing > CN_PACKET_BUFFER_SIZE)
if (base + trailing > CN_PACKET_BUFFER_SIZE - 8)
return false;
// everything is a-ok!
@@ -90,9 +110,9 @@ 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 / (size_t)npayloads < plsize)
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
return false;
// it's safe to multiply
@@ -107,6 +127,9 @@ inline bool validInVarPacket(size_t base, int32_t npayloads, size_t plsize, size
return true;
}
void printSocketError(const char *func);
bool setSockNonblocking(SOCKET listener, SOCKET newSock);
namespace CNSocketEncryption {
// you won't believe how complicated they made it in the client :facepalm:
static constexpr const char* defaultKey = "m@rQn~W#";
@@ -120,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 {
@@ -150,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);
@@ -166,18 +195,28 @@ 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;
typedef void (*TimerHandler)(CNServer* serv, uint64_t time);
typedef void (*TimerHandler)(CNServer* serv, time_t time);
// timer struct
struct TimerEvent {
TimerHandler handlr;
uint64_t delta; // time to be added to the current time on reset
uint64_t scheduledEvent; // time to call handlr()
time_t delta; // time to be added to the current time on reset
time_t scheduledEvent; // time to call handlr()
TimerEvent(TimerHandler h, uint64_t d): handlr(h), delta(d) {
TimerEvent(TimerHandler h, time_t d): handlr(h), delta(d) {
scheduledEvent = 0;
}
};
@@ -185,9 +224,13 @@ struct TimerEvent {
// in charge of accepting new connections and making sure each connection is kept alive
class CNServer {
protected:
std::list<CNSocket*> connections;
std::unordered_map<SOCKET, CNSocket*> connections;
std::mutex activeCrit;
const size_t STARTFDSCOUNT = 8; // number of initial PollFD slots
std::vector<PollFD> fds;
std::string serverType = "invalid";
SOCKET sock;
uint16_t port;
socklen_t addressSize;
@@ -196,6 +239,9 @@ protected:
bool active = true;
void addPollFD(SOCKET s);
void removePollFD(int i);
public:
PacketHandler pHandler;
@@ -204,8 +250,9 @@ public:
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);
virtual void onStep(); // called every 2 seconds
virtual void onStep();
};

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

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

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

@@ -0,0 +1,29 @@
/*
* core/CNShared.hpp
* There's some data shared between the Login Server and the Shard Server. Of course all of this needs to be thread-safe. No mucking about on this one!
*/
#pragma once
#include <map>
#include <string>
#include "Player.hpp"
/*
* Connecions time out after 5 minutes, checked every 30 seconds.
*/
#define CNSHARED_TIMEOUT 300000
#define CNSHARED_PERIOD 30000
struct LoginMetadata {
uint64_t FEKey;
int32_t playerId;
time_t timestamp;
};
namespace CNShared {
void storeLoginMetadata(int64_t sk, LoginMetadata *lm);
LoginMetadata* getLoginMetadata(int64_t sk);
void pruneLoginMetadata(CNServer *serv, time_t currTime);
}

View File

@@ -1,6 +1,6 @@
/*
CNStructs.hpp - defines some basic structs & useful methods for packets used by FusionFall based on the version defined
*/
* CNStructs.hpp - defines some basic structs & useful methods for packets used by FusionFall based on the version defined
*/
#pragma once
@@ -24,15 +24,30 @@
#include <locale>
#include <codecvt>
// yes this is ugly, but this is needed to zero out the memory so we don't have random stackdata in our structs.
// yes this is ugly, but this is needed to zero out the memory so we don't have random stackdata in our structs.
#define INITSTRUCT(T, x) T x; \
memset(&x, 0, sizeof(T));
#define INITVARPACKET(_buf, _Pkt, _pkt, _Trailer, _trailer) uint8_t _buf[CN_PACKET_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)
// 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);
size_t U8toU16(std::string src, char16_t* des); // returns number of char16_t that was written at des
uint64_t getTime();
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();
void terminate(int);
// The PROTOCOL_VERSION definition is defined by the build system.
#if !defined(PROTOCOL_VERSION)
@@ -41,6 +56,10 @@ uint64_t getTime();
#include "structs/0728.hpp"
#elif PROTOCOL_VERSION == 104
#include "structs/0104.hpp"
#elif PROTOCOL_VERSION == 1013
#include "structs/1013.hpp"
#else
#error Invalid PROTOCOL_VERSION
#endif
sSYSTEMTIME timeStampToStruct(uint64_t time);

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