mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2025-10-23 21:40:13 +00:00
Compare commits
904 Commits
1.1
...
02c1c681dd
| Author | SHA1 | Date | |
|---|---|---|---|
| 02c1c681dd | |||
|
|
225515ec21 | ||
|
|
f436108d24 | ||
|
|
1781ac6b03 | ||
|
|
b0e2391f24 | ||
|
|
6f88613ede | ||
|
|
78e4779db6 | ||
|
|
47dbc6d35e | ||
|
|
b780f5ee60 | ||
|
|
003186d97a | ||
|
|
6d2f120305 | ||
|
|
51615db230 | ||
|
|
233d21ecd7 | ||
|
|
54327b0c23 | ||
|
|
fa8c1e73d1 | ||
|
|
aeac57ebf7 | ||
|
|
632406e93b | ||
|
837f109752
|
|||
|
c11cfebdb1
|
|||
|
20367d77f0
|
|||
|
8d04f31c61
|
|||
|
|
44560a46b7 | ||
|
|
21d280147c | ||
|
|
b765821552 | ||
| e61682dfb2 | |||
|
|
d9ebb4e3ef | ||
|
|
73c610b471 | ||
|
|
3e6bfea3fe | ||
|
|
cd265af8e0 | ||
|
|
38c68f351b | ||
| edfbe4d005 | |||
| 96c430c994 | |||
|
|
4592fc42af | ||
|
|
70a27afad1 | ||
|
|
6cfb3bf532 | ||
| 9b2a65f8fd | |||
| 6a69388822 | |||
| 2924a27eb4 | |||
| ba20f5a401 | |||
|
|
eb88fa05cb | ||
|
|
0b73cef187 | ||
|
|
7af39b3d04 | ||
|
|
33206b1207 | ||
|
|
e325f7a40b | ||
|
|
82bee2051a | ||
|
|
4ece1bb89b | ||
|
|
31677e2638 | ||
|
|
d32827b692 | ||
|
|
13c009b448 | ||
|
|
a032497bed | ||
|
|
3b6b61d087 | ||
|
|
6d760f5bce | ||
|
|
2a622f901c | ||
|
|
03d28bf4e4 | ||
|
|
4b834579c5 | ||
|
|
07fe8ca367 | ||
|
|
2f3f8a3951 | ||
| 4f890a9c07 | |||
| 8517e0c7de | |||
| 5fb0cbbcf7 | |||
| 55e9f6531d | |||
|
|
7726357fbe | ||
|
|
564c275d51 | ||
|
|
3ce9ae5f77 | ||
|
|
7c5b9a8105 | ||
|
|
258ff35e20 | ||
|
|
ab480d88f1 | ||
|
|
89772d763b | ||
| bd0cc3c212 | |||
| c636c538eb | |||
| d3bef95a7f | |||
|
|
650f947451 | ||
|
|
b12aecad63 | ||
|
|
5bf0c8f3ea | ||
|
|
2ddc956c9b | ||
|
|
4f0ae027a5 | ||
| 23ab908366 | |||
| be6a4c0a5d | |||
| 8eb1af20c8 | |||
| e73daa0865 | |||
| 743a39c125 | |||
| a9af8713bc | |||
| 4825267537 | |||
| a92cfaff25 | |||
| abcfa3445b | |||
| 2bf14200f7 | |||
| 876a9c82cd | |||
| fb5b0eeeb9 | |||
| 7aabc507e7 | |||
| 2914b95cff | |||
| dbd2ec2270 | |||
| 50e00a6772 | |||
| 7471bcbf38 | |||
| 100b4605ec | |||
| 741b898230 | |||
| 3f44f53f97 | |||
| d92b407349 | |||
| 9b3e856a05 | |||
| eb8e54c1f0 | |||
| 1ba0f5e14a | |||
| 12dde394c0 | |||
| b1eea6d4fe | |||
| f126b88781 | |||
| 2dbe2629c1 | |||
| 271eef83d3 | |||
| ca0d608a87 | |||
| 741bfb675b | |||
| c5dd745aa1 | |||
| 998b12617e | |||
| 129d1c2fe3 | |||
|
|
1bd4d2fbee | ||
| 63d4087488 | |||
| abda9dc158 | |||
|
|
7b7d8bce45 | ||
|
|
a9942eadab | ||
|
|
36638b1522 | ||
|
|
685cee2561 | ||
|
|
b683152fbf | ||
|
|
86576d48f6 | ||
|
|
4e1767ad58 | ||
|
|
4354cab7e3 | ||
| 4f6979f236 | |||
| 1404fa0bb7 | |||
| 7f65ec5b96 | |||
| 041908ddda | |||
| 57c9f139a2 | |||
| d3af99fcef | |||
| 94af318139 | |||
| 91f9a2085b | |||
| 00865e1c7b | |||
| 28bfd14362 | |||
| 6412a9a89e | |||
| f376c68115 | |||
| 3c6afa0322 | |||
| 384a2ece78 | |||
|
|
f4a7ab7373 | ||
| bc1153c97e | |||
| c6ffcd4804 | |||
| b3c844650b | |||
| 13bd299de4 | |||
| 1e3d183f9a | |||
| dfe596447b | |||
| 9297e82589 | |||
| 4319ee57a0 | |||
| 09e452a09d | |||
| 3c1e08372d | |||
| 05d6174351 | |||
| 9ab998688c | |||
| 7249b54127 | |||
| 4fa18a9642 | |||
|
|
8a294cb2be | ||
| 57e9834786 | |||
| 70c3650ee1 | |||
| e2c85aa03f | |||
|
|
ed285e5d24 | ||
| 0883ec4aae | |||
|
|
2eb64540d1 | ||
| bb4029a9bf | |||
| 25ce0f6d82 | |||
|
|
bab17eb23f | ||
|
|
aaaf03128a | ||
|
|
558d056bcf | ||
|
|
0accd1f345 | ||
|
|
bb12a60e04 | ||
|
|
8326ea6e26 | ||
| 81cc19f985 | |||
| 19fd4ecb83 | |||
|
|
243e4f6d50 | ||
|
|
6f59001be1 | ||
|
|
e5d9e7217e | ||
| 5c1bb0acc9 | |||
|
|
32daa68458 | ||
|
|
2d7aa3c536 | ||
|
|
eb8ec85746 | ||
|
|
a90ba9ea08 | ||
|
|
974b67d4b6 | ||
|
|
6ae4ab2cbf | ||
|
|
9fb41342b3 | ||
|
|
0ccc66208d | ||
|
|
ebd3b7b75a | ||
|
|
d41122157f | ||
|
|
917407f164 | ||
|
|
c393bf7af2 | ||
|
|
72b62cd5a4 | ||
|
|
e508a06eca | ||
|
|
f71d2581bd | ||
|
|
89a32ac9a4 | ||
|
|
c7e2e66a51 | ||
|
|
37b1d11948 | ||
|
|
c960b06227 | ||
|
|
2721f21427 | ||
|
|
d5e65fda3c | ||
|
|
5f29ea93d8 | ||
|
|
36cb32454d | ||
|
|
af8dd61967 | ||
|
|
26894c8a69 | ||
|
|
bf12ed4c47 | ||
|
|
59303ba30d | ||
|
|
b2a8b86e4c | ||
|
|
0c05fc4add | ||
|
|
c415db3fd3 | ||
|
|
183586afe4 | ||
|
|
e546d3948c | ||
|
|
a0e758f5b7 | ||
|
|
f58c6b72b3 | ||
|
|
438eba4aa8 | ||
|
|
805fd93b3c | ||
|
|
4bcf3af90f | ||
|
|
4eeb93ad51 | ||
|
|
e761c700dc | ||
|
|
14562f889e | ||
|
|
dd3066849b | ||
|
|
91dd5d5280 | ||
| 2658ed5900 | |||
|
|
1c3e1d83de | ||
|
|
c240cc005f | ||
| 33ea5af8b7 | |||
|
|
55f8dc94ce | ||
|
|
b99cab58f7 | ||
|
|
21e283bedb | ||
|
|
5517a358ea | ||
|
|
4eaf3b2f08 | ||
|
|
44fd66b511 | ||
|
|
001a0b8d4b | ||
|
|
20b05a72a0 | ||
|
|
78b87d0f61 | ||
|
|
527ca817d5 | ||
|
|
80b11b4364 | ||
|
|
6f636b77f2 | ||
|
|
8a871f7045 | ||
|
|
1779d69078 | ||
|
|
30de9f668c | ||
|
|
78b17aea72 | ||
|
|
aa028392f0 | ||
|
|
f0e21b5051 | ||
|
|
80d965944c | ||
|
|
2e34440d2e | ||
|
|
e9709805b7 | ||
|
|
fa7c88e214 | ||
|
|
588e941d3c | ||
|
|
f5600912cb | ||
|
|
fde4a5ff34 | ||
| 9b84d9dc4d | |||
| 48fb510b53 | |||
|
|
fd965fbf03 | ||
| 65462d01e3 | |||
| 3325397d17 | |||
| 22678fcfc2 | |||
| 0c8e209360 | |||
| 224ffe05e7 | |||
| 49f1cb0f00 | |||
|
|
a57953393d | ||
|
|
b428eb08e9 | ||
|
|
4a22449f5e | ||
| 7f9cdfc9ae | |||
| 8afe175bd1 | |||
| 0f687cc6b3 | |||
| 55b140f673 | |||
| 688f13e649 | |||
| ef7d0148c6 | |||
| 7c7d9f1be8 | |||
|
|
919c14be0d | ||
| 124ea33959 | |||
| 69266d1cda | |||
| 574f0cab09 | |||
| 610a683804 | |||
| a55a34e09a | |||
| e9bc2fe561 | |||
| cee09f6344 | |||
| 04c56ce426 | |||
| 2017b38e23 | |||
| c5776b9322 | |||
|
|
dd41d5b610 | ||
|
|
8981ad8c14 | ||
| df1ac82300 | |||
| ec67cc6527 | |||
| 2024fb4969 | |||
| ae279100d7 | |||
| e92a5a2f8b | |||
| ce197d7db3 | |||
| f9c2587557 | |||
| 2d7129111a | |||
| 4cd3a3dabd | |||
| f7e9cc2cea | |||
| 89eb0b140b | |||
| f5a34b9a3d | |||
| ffe5947925 | |||
| 0fbdb1dad2 | |||
| d781fae3ba | |||
| 3445c0bbc3 | |||
| 540c37a523 | |||
| 33a26cda7c | |||
| dc6de46a1f | |||
| c5e08b81da | |||
| 5e569d4324 | |||
| f2b1a84ef4 | |||
| d5fe1cc513 | |||
| 81c2a2a8b3 | |||
| f7c84c62ed | |||
| da8dde9818 | |||
|
|
29dbe83a0b | ||
| 5fdef50f0f | |||
|
|
b04c66dea7 | ||
|
|
f0f3eaf749 | ||
| 217168fe50 | |||
|
|
04a17ed862 | ||
| 74af1ad173 | |||
|
|
b0697f12a3 | ||
| 34bd7c102f | |||
|
|
9e30e55669 | ||
| 46b6d9fcc7 | |||
| 2bf3fd0975 | |||
| 442f85c7a6 | |||
| b87229aa65 | |||
| deca220d43 | |||
| 74e06f1084 | |||
| ddc7caf959 | |||
| 6baa0c5b07 | |||
| d4eaf83354 | |||
| 47d13ce39e | |||
| 2b95bc660c | |||
| 0e3fac4d34 | |||
|
|
89e4b2be22 | ||
|
|
28543641bb | ||
|
|
888f0e77f9 | ||
| 4516227a7b | |||
|
|
954cfabde5 | ||
| 52e3c3bcd7 | |||
| 966bd3edd2 | |||
| dab204ddaf | |||
| bad8ef1d10 | |||
| a12acbb68f | |||
| c78b3ca69f | |||
| 55431362a7 | |||
| 07a930fe1c | |||
| 4060bf25b0 | |||
| 9a79ab3927 | |||
| fc45775666 | |||
| 81d0964971 | |||
| 868dc8485e | |||
| 26f4767082 | |||
| d97444cca5 | |||
| ee978e8bc9 | |||
|
|
bdf283ae4f | ||
| f8129b91cb | |||
| afea9f436f | |||
| 7985fc475b | |||
| 959a708176 | |||
| 44fbb8e81f | |||
| e02ef55844 | |||
|
|
8bbf40ac95 | ||
|
|
7fe0e19bb0 | ||
|
|
82d5455da6 | ||
|
|
52389c2c69 | ||
|
|
ae75324153 | ||
|
|
a1a5815f1f | ||
|
|
772f80188e | ||
|
|
f28c643b48 | ||
|
|
5f82658c8d | ||
| ff75aa6693 | |||
| 50b2bdcb16 | |||
|
|
34dbb59fb1 | ||
|
|
a74c9be2ff | ||
| fcdea2e723 | |||
| 90191fd494 | |||
|
|
effbbd9a5e | ||
| bc7f4883a2 | |||
| e0808ffcbd | |||
| 9057f31bff | |||
| 0a9f637123 | |||
| 027f513a23 | |||
| a99f95d15f | |||
|
|
49158360ca | ||
| 0104bc9329 | |||
| 4fc5c092f8 | |||
| 8d1bc94b7e | |||
| 322e354f5b | |||
|
|
8a2073d081 | ||
|
|
e915c54ed0 | ||
|
|
e953b51229 | ||
|
|
b9013149f3 | ||
|
|
e6da454c73 | ||
|
|
598c7ce1d0 | ||
|
|
ea47f67b2b | ||
|
|
91ea8be72e | ||
|
|
b81a3761b6 | ||
|
|
45b8f8f581 | ||
|
|
cfc6a94c7a | ||
| c3d9883ddb | |||
|
|
ea12ec9607 | ||
|
|
2e173df2ca | ||
|
|
1fb48536c2 | ||
|
|
002bfffb62 | ||
|
|
140227406c | ||
| 26d0623d07 | |||
| f8a359dfe9 | |||
|
|
2f44243abb | ||
| e3561e9d15 | |||
| 80dabf4406 | |||
|
|
d3e5b9c485 | ||
| f7a6615379 | |||
|
|
618a8d0a9f | ||
| d25e7ca4fc | |||
| 3359ca0c3e | |||
| 0dd478b9f0 | |||
|
|
d03c4f109f | ||
|
|
792a317b48 | ||
| f74c40cf69 | |||
| 442d7853a5 | |||
|
|
f1aa2c19ef | ||
| fcd9b55ea7 | |||
|
|
b1375c69f5 | ||
|
|
51c3e01062 | ||
|
|
f1a2723274 | ||
|
|
5431d21d27 | ||
|
|
57c28d7539 | ||
| 6937ff86a4 | |||
| 454e0284af | |||
|
|
94b7864b02 | ||
|
|
166bfdfc4f | ||
|
|
156e9bf902 | ||
|
|
c8ff130b78 | ||
|
|
ec23e72215 | ||
|
|
a1274756ce | ||
|
|
05f4746af4 | ||
|
|
04112377ea | ||
|
|
bd3a91e530 | ||
|
|
66ecc45fce | ||
|
|
7aef973ef1 | ||
|
|
a12faac0e2 | ||
|
|
4dc48198ab | ||
|
|
3e855cbdac | ||
|
|
889fc985c4 | ||
|
|
c709d458f4 | ||
|
|
0e016646ef | ||
|
|
960f2dd10c | ||
|
|
2bad1252d3 | ||
|
|
31ac9d2e3b | ||
|
|
eeb3b1ee61 | ||
|
|
4bf35e5239 | ||
|
|
2f5c2a8764 | ||
|
|
34ca36062c | ||
|
|
a1062f220b | ||
|
|
c2f640fd97 | ||
|
|
143bb00ac0 | ||
|
|
b947ff65cf | ||
| 1474ff10ac | |||
| 974941f4fa | |||
|
|
2834891727 | ||
| ada8af0b82 | |||
| 86e0b1bc13 | |||
|
|
d5409ed3f1 | ||
| 811c9d4d5c | |||
|
|
a48fb3a8e8 | ||
| 27f396af7e | |||
| 8ebabac7c0 | |||
| 02c5df5c1b | |||
| dd6fbfb683 | |||
| 92307063fc | |||
| 53d8cb67ba | |||
| 6b257887b7 | |||
| ec7cba644c | |||
| 269315ca09 | |||
| 661061b4eb | |||
| 3a2b488f33 | |||
| 721b3f440f | |||
|
|
b04c377d7c | ||
|
|
794856a63c | ||
|
|
856a90abcf | ||
| 88d904e302 | |||
| 46552307cd | |||
| 26024de866 | |||
|
|
85dcdd4cc5 | ||
|
|
6e7129bf6f | ||
|
|
1ca8094628 | ||
|
|
543d0a7afd | ||
|
|
4c398895aa | ||
| e899928928 | |||
| c8b011913a | |||
| 15b63f3cbd | |||
| ce1a5a7664 | |||
| c6112d04da | |||
|
|
73f8179836 | ||
|
|
86f17b6525 | ||
| fed3eca378 | |||
| 840cba6a9e | |||
| fb9c4140b6 | |||
| 657061083e | |||
| 8a86c75747 | |||
| 046e7bb6f1 | |||
| 1e822f7a6c | |||
| c43a3d64fb | |||
|
|
7c5d7a70cc | ||
|
|
c1941654b6 | ||
|
|
d2d6171d04 | ||
| 58952be47e | |||
| 5771cd014a | |||
|
|
c9754902b9 | ||
|
|
c508016ca3 | ||
|
|
4863d29590 | ||
|
|
128aad89d3 | ||
| 858fbf40be | |||
|
|
1d7f8bd133 | ||
| a9ad399bc2 | |||
| 963205fad6 | |||
| b836952356 | |||
| 6fb652f642 | |||
| 2cde3e34f6 | |||
| 1371a6da77 | |||
| d2e89851d6 | |||
|
|
7cc0a0fc82 | ||
| c343092bd5 | |||
|
|
36d6231da4 | ||
|
|
7851866d13 | ||
|
|
c2ab5c9d02 | ||
| 166eb5125f | |||
| 26ca4d8671 | |||
| 28276d2229 | |||
| 2b25b17bd8 | |||
| f7c0596a4c | |||
| fe7ec44554 | |||
| 299fc1b461 | |||
| 2acb90f2d2 | |||
|
|
75d33aff3e | ||
|
|
8073c68bd5 | ||
|
|
15dd0a2fc3 | ||
|
|
71d1212877 | ||
|
|
57060e9b6f | ||
|
|
faf2a0ee7d | ||
|
|
fa7b6e6145 | ||
|
|
478bcd5338 | ||
|
|
c7d3870a60 | ||
|
|
8351596763 | ||
|
|
e3568ea506 | ||
|
|
a9a8d96321 | ||
|
|
b0aea27418 | ||
|
|
872425640d | ||
|
|
ea5b7104be | ||
|
|
d85d9d4b12 | ||
|
|
7e08bc60ce | ||
|
|
86e6937342 | ||
|
|
ec8abfa004 | ||
|
|
51a687c7db | ||
|
|
657306e0a1 | ||
|
|
eee8aab888 | ||
|
|
5c6d7d6055 | ||
|
|
4760d91ccd | ||
|
|
db98af9775 | ||
|
|
f91f9786d1 | ||
|
|
71d9cab72e | ||
|
|
717e5eb78f | ||
|
|
e7301f46ef | ||
|
|
85113a667f | ||
|
|
98ae236c08 | ||
|
|
6ff762ba57 | ||
|
|
73ef5fa5ff | ||
|
|
7d81035306 | ||
|
|
8a0d0e0e4c | ||
|
|
0f1d9cdf1c | ||
|
|
07a1927b9f | ||
|
|
e0858a42b2 | ||
|
|
dab536cb6a | ||
| f1d04cec01 | |||
| 5e8b6eec6e | |||
| 883a1c17e6 | |||
|
|
e9ffbe6148 | ||
|
|
d1baa0d9f9 | ||
|
|
71d4f331b5 | ||
|
|
665f28313a | ||
|
|
c5fa397724 | ||
|
|
95b385dee1 | ||
|
|
82b505a737 | ||
|
|
dae3b24093 | ||
|
|
e50a4c2edd | ||
|
|
5cbb8538c0 | ||
|
|
d2e776b672 | ||
|
|
b08fb52272 | ||
|
|
0075457f81 | ||
|
|
12baece1b2 | ||
|
|
eaeeae8d62 | ||
|
|
c77f99e849 | ||
|
|
d17890af68 | ||
|
|
6c1d8c3527 | ||
|
|
59ab81d3c6 | ||
|
|
9cee8f2c87 | ||
|
|
a483b0fb44 | ||
|
|
8ad3f3aabd | ||
|
|
b22ba781c8 | ||
|
|
cc74b01f72 | ||
|
|
4d1d77ceaf | ||
|
|
7135767cc4 | ||
|
|
0ecf76c5ec | ||
|
|
9087baae3c | ||
|
|
95b5da8932 | ||
|
|
f733aa60f0 | ||
|
|
d102fabc2f | ||
|
|
6d4afd0c6a | ||
|
|
8003518e18 | ||
|
|
859b24229a | ||
|
|
674d5112f3 | ||
|
|
de99522340 | ||
|
|
c7006b46ed | ||
|
|
45ed99ae35 | ||
|
|
f2ff4c7f4d | ||
| 2744ed64e3 | |||
|
|
90754665a0 | ||
|
|
4454faffc3 | ||
|
|
34f2aef248 | ||
|
|
121c65d7ea | ||
|
|
f53de8d521 | ||
|
|
8a68958ed4 | ||
|
|
f0ded0886c | ||
|
|
d7a8d2d453 | ||
|
|
5293573116 | ||
|
|
d505b09e98 | ||
|
|
8f90fdaac4 | ||
|
|
3a55a9b66f | ||
|
|
36e9370ff4 | ||
|
|
3f5a9c8811 | ||
|
|
30de5c1734 | ||
|
|
c7591c6ce2 | ||
|
|
7be79010fc | ||
|
|
803f1a246a | ||
|
|
262dca7dd6 | ||
|
|
26460c0167 | ||
| 83c378c9c2 | |||
| a1145aced4 | |||
| 1a405034af | |||
| d21f727e9d | |||
|
|
7b4fab8c6b | ||
|
|
2af33da4e8 | ||
|
|
f3b9ecd791 | ||
|
|
609d3cdb99 | ||
|
|
d840b0bbd0 | ||
|
|
4da178d16c | ||
|
|
0d65fc2653 | ||
|
|
6d97aaa1d0 | ||
|
|
4ab686bc46 | ||
|
|
2302c28ac5 | ||
| c8497a4856 | |||
| 177c5f0f17 | |||
| 2782706355 | |||
|
|
a969988b5c | ||
|
|
bf3c19764b | ||
| cc06fdcf60 | |||
| 3b5af415fb | |||
|
|
2b650b0bed | ||
|
|
512647974d | ||
| c9f9b093f4 | |||
|
|
49d8ed2e36 | ||
| 10534886b8 | |||
|
|
d713fafb1c | ||
| e97b58ccaf | |||
| 55be58cc24 | |||
|
|
deb3e5b897 | ||
| 4a5857a126 | |||
|
|
f7e7f99017 | ||
|
|
6d9d66954e | ||
| bbd695cad1 | |||
| 3ce8cf2129 | |||
| 7f716c7278 | |||
|
|
dd54668697 | ||
|
|
ab5857e7e2 | ||
|
|
da725d21e6 | ||
|
|
6473951b9a | ||
|
|
3050801399 | ||
|
|
efd729710f | ||
|
|
85530ef57f | ||
|
|
b87f20e2dc | ||
|
|
d4aed0abf4 | ||
|
|
c1fd51b721 | ||
|
|
bd34bb294c | ||
| 5a80c53e79 | |||
| 5784e77654 | |||
| 6ee5e6d1ae | |||
| 3586b76888 | |||
| 843b2e6e38 | |||
| a8c8065920 | |||
| 63414ea9a2 | |||
| 599bbedd8c | |||
| c792fb9d0d | |||
| f8d64234d7 | |||
| 901e011740 | |||
|
|
a53f38b87d | ||
| f3b6f9619b | |||
| 4d687a82ea | |||
| d99dad261c | |||
|
|
1564cc7724 | ||
| 2b9d0f6bab | |||
| 9f280c2c31 | |||
| bae834fefa | |||
| 1fe23b97fd | |||
| 0d0332e551 | |||
| 7a83a3b45c | |||
| 606384445c | |||
| 7caa73caca | |||
| 6e3d0868cb | |||
|
|
5ed332d836 | ||
|
|
42fc018097 | ||
|
|
1b68b5e2e2 | ||
|
|
5009fe1994 | ||
|
|
4873eba160 | ||
| d4d0f388c4 | |||
| ce58411ff8 | |||
| 661070dc3a | |||
|
|
b8f586bc10 | ||
| 131eb94919 | |||
| 755bb75306 | |||
| 5015e2575d | |||
|
|
a9837d6c1b | ||
|
|
47da895544 | ||
| a852c26e5e | |||
| 316239dadc | |||
| f5939353b1 | |||
| f82d203377 | |||
|
|
cbd04c2ce6 | ||
|
|
1b55ab44e3 | ||
| 21b7500e13 | |||
| 35a2110698 | |||
| 8a144a359f | |||
| 2fe4b2bac1 | |||
| 839f9a813c | |||
| 4fe4aeb0d3 | |||
| 600c26024b | |||
|
|
3c734e3e76 | ||
| 4cd7b7cb53 | |||
| 8ff97ec0b3 | |||
| 5f65c1530b | |||
|
|
2c831ee115 | ||
|
|
941e986ee1 | ||
|
|
1eb806af58 | ||
| ab990116a2 | |||
| fb281b0237 | |||
| 3f35d2e960 | |||
| 884b844d65 | |||
| 4079806436 | |||
| efb3df7133 | |||
| c9be0e5402 | |||
| 97c2c532f1 | |||
| a324f3fda9 | |||
| 6ea47ddb56 | |||
| 2b4a1387f9 | |||
| dacae8d6de | |||
|
|
b03cc563eb | ||
|
|
dd374b2ea1 | ||
| f8f2088e38 | |||
| 062302a7aa | |||
| a5d3160588 | |||
| 4fea2ae896 | |||
|
|
56a92d302f | ||
|
|
0ea5712f8c | ||
| b4fb449e69 | |||
| 4fa6618abb | |||
| 43d268e142 | |||
| 9657aaf202 | |||
| dccd92aff9 | |||
| 1b35aab958 | |||
| 6b577ed642 | |||
|
|
0931cf1fbc | ||
|
|
805c64eff0 | ||
|
|
8c63cd575c | ||
| 5068b38c5e | |||
| 231a4a441b | |||
|
|
d4f1515f5d | ||
| e5a24bcb70 | |||
| 874479d1cf | |||
| 8f84c4c2f8 | |||
|
|
320a82997a | ||
|
|
d87306930d | ||
|
|
86c1cbd0f2 | ||
| 279cb78d5f | |||
| 72d625fd8d | |||
|
|
db33ca2bbb | ||
| cfb3d25bc5 | |||
| 1f18104a6f | |||
| 006d1000c7 | |||
|
|
1874f1081b | ||
|
|
df936e8c9c | ||
|
|
72c16587e0 | ||
| c33f218e56 | |||
| 4caca07856 | |||
| 63c14aff58 | |||
| 78930916ad | |||
|
|
9cfced88c9 | ||
|
|
f2596bfb6a | ||
|
|
65bd2d120b | ||
|
|
7bcdc111da | ||
|
|
016c48645e | ||
|
|
09f1f67778 | ||
|
|
7dfc888552 | ||
|
|
6f05f0f2c8 | ||
| c722044bf5 | |||
|
|
076f89927d | ||
| 95a79ec815 | |||
|
|
7ba9b9a54f | ||
| ba5998d53a | |||
| ac1fd1e5be | |||
| 94ab5b8b64 | |||
|
|
e0e474924d | ||
| 8896a103ba | |||
|
|
0931c88541 | ||
|
|
5a58908462 | ||
|
|
00f64ce992 | ||
|
|
153b3a9ef5 | ||
|
|
adf017b07c | ||
| 5d8bb7f8a5 | |||
|
|
2c8243e136 | ||
| 12fbdc9621 | |||
| a768a4f539 | |||
| d6357197d3 | |||
| 4cc1cf4f7e | |||
| b67a0b6946 | |||
|
|
5e0948ea93 | ||
|
|
90134cd1fa | ||
| 321dca3f79 | |||
| 113ecc8f60 | |||
| dc9de5a54a | |||
| 0fc072d591 | |||
| 24341c578a | |||
|
|
a05bb15697 | ||
|
|
135424b855 | ||
|
|
cb984c029b | ||
|
|
a5ffe26c44 | ||
|
|
6a78a301c9 | ||
|
|
a5c40b66f5 | ||
|
|
94583e534b | ||
|
|
ff7c78d545 | ||
|
|
77df7b7160 | ||
|
|
6eb21e6d67 | ||
| 228a181b74 | |||
|
|
27df1bd7d0 | ||
|
|
6a05ce4504 | ||
|
|
c6ec1c46c2 | ||
|
|
d1c5e272a8 | ||
|
|
9bb19efc99 | ||
|
|
7757238a47 | ||
|
|
4d437bcb34 | ||
|
|
5dbca0b7b1 | ||
| ce9285bab5 | |||
|
|
cd7fec2d5b | ||
| d9d781c37d | |||
|
|
b929d12902 | ||
| 9f78735caa | |||
| 31ef03610d | |||
| 22e3e9e4de | |||
| f4db0830ba | |||
| 001564a257 | |||
| e79f179628 | |||
| 027b783571 | |||
| e03da83ff3 | |||
| 5efc8ac089 | |||
| efda6673b5 | |||
| f7571607ba | |||
|
|
501d153894 | ||
| 4d21410980 | |||
|
|
148d90f4f1 | ||
|
|
a976fef2b4 | ||
|
|
da8c833587 | ||
|
|
c91022030c | ||
|
|
f55cc8f36d | ||
| 9cc5f3e4d5 | |||
| 131997f34f | |||
| ed86bc9160 | |||
|
|
38d5998a6e | ||
|
|
c7189a5cef | ||
|
|
de15e2004b | ||
|
|
480cca82fa | ||
|
|
3d83f93167 | ||
|
|
1d9a7139a8 | ||
|
|
2fd7a8c6fc | ||
|
|
fc57cae37d | ||
|
|
3cfec7aab3 | ||
|
|
29e53117e7 | ||
| c1ac2250a0 | |||
| a4716b0164 | |||
|
|
91f512d740 | ||
|
|
4880e4af12 | ||
| fe370df534 | |||
|
|
2b1a028b3d | ||
| be99714495 | |||
| 4c06163b51 | |||
| 0c97969757 | |||
|
|
4e7352da66 | ||
| 5747c24479 | |||
| 579aa9d31d | |||
|
|
3865249387 | ||
| 468840c9ea | |||
| 52f02168bc | |||
| ddb5f782b7 | |||
| 3665dc2c93 | |||
| ae654f996c | |||
| e33b7f20e9 | |||
| 5b49e71de7 | |||
| 3172724596 | |||
| 8887c6349b | |||
|
|
6e0b101a76 | ||
| 29cde56fb1 | |||
| e65f07780b | |||
| eb1ad6bb37 | |||
|
|
e409b8bb39 | ||
| 45a33758a5 | |||
|
|
266fddbffa | ||
| e90ae10746 | |||
| b797993014 |
@@ -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
5
.gitattributes
vendored
Normal 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
|
||||
147
.github/workflows/check-builds.yaml
vendored
Normal file
147
.github/workflows/check-builds.yaml
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
name: Check Builds
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- src/**
|
||||
- vendor/**
|
||||
- .github/workflows/check-builds.yaml
|
||||
- CMakeLists.txt
|
||||
- Makefile
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- src/**
|
||||
- vendor/**
|
||||
- CMakeLists.txt
|
||||
- Makefile
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ubuntu-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set environment
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
run: sudo apt install clang cmake snap -y && sudo snap install powershell --classic
|
||||
- name: Check compilation
|
||||
run: |
|
||||
$versions = "104", "728", "1013"
|
||||
|
||||
foreach ($version in $versions) {
|
||||
Write-Output "Cleaning old output"
|
||||
Invoke-Expression "make clean"
|
||||
if ($LASTEXITCODE -ne "0") {
|
||||
Write-Error "make clean failed for version $version" -ErrorAction Stop
|
||||
}
|
||||
Write-Output "Building version $version"
|
||||
Invoke-Expression "make -j8 PROTOCOL_VERSION=$version"
|
||||
if ($LASTEXITCODE -ne "0") {
|
||||
Write-Error "make failed for version $version" -ErrorAction Stop
|
||||
}
|
||||
Rename-Item -Path "bin/fusion" -newName "$version-fusion"
|
||||
Write-Output "Built version $version"
|
||||
}
|
||||
Copy-Item -Path "sql" -Destination "bin/sql" -Recurse
|
||||
Copy-Item -Path "config.ini" -Destination "bin"
|
||||
shell: pwsh
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
|
||||
path: bin
|
||||
|
||||
windows-build:
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- name: Set environment
|
||||
run: $s = $env:GITHUB_SHA.subString(0, 7); echo "SHORT_SHA=$s" >> $env:GITHUB_ENV
|
||||
shell: pwsh
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Check compilation
|
||||
run: |
|
||||
$versions = "104", "728", "1013"
|
||||
$configurations = "Release"
|
||||
# "Debug" builds are disabled, since we don't really need them
|
||||
|
||||
$vsPath = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise"
|
||||
|
||||
Import-Module "$vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
|
||||
Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation
|
||||
|
||||
Invoke-Expression "vcpkg install sqlite3:x64-windows"
|
||||
Invoke-Expression "vcpkg integrate install"
|
||||
|
||||
foreach ($version in $versions) {
|
||||
if (Test-Path -LiteralPath "build") {
|
||||
Remove-Item "build" -Recurse
|
||||
Write-Output "Deleted existing build folder"
|
||||
}
|
||||
Invoke-Expression "cmake -B build -DPROTOCOL_VERSION=$version -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake"
|
||||
if ($LASTEXITCODE -ne "0") {
|
||||
Write-Error "cmake generation failed for version $version" -ErrorAction Stop
|
||||
}
|
||||
Write-Output "Generated build files for version $version"
|
||||
|
||||
foreach ($configuration in $configurations) {
|
||||
Write-Output "Building version $version $configuration"
|
||||
Invoke-Expression "msbuild build\OpenFusion.sln /maxcpucount:8 /p:BuildInParallel=true /p:CL_MPCount=8 /p:UseMultiToolTask=true /p:Configuration=$configuration"
|
||||
if ($LASTEXITCODE -ne "0") {
|
||||
Write-Error "msbuild build failed for version $version" -ErrorAction Stop
|
||||
}
|
||||
Rename-Item -Path "bin/$configuration" -newName "$version-$configuration"
|
||||
Write-Output "Built version $version $configuration"
|
||||
Copy-Item -Path "sql" -Destination "bin/$version-$configuration/sql" -Recurse
|
||||
Copy-Item -Path "config.ini" -Destination "bin/$version-$configuration"
|
||||
}
|
||||
}
|
||||
shell: pwsh
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@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-22.04
|
||||
needs: [windows-build, ubuntu-build]
|
||||
env:
|
||||
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}
|
||||
ENDPOINT: ${{ secrets.ENDPOINT }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- run: |
|
||||
GITDESC=$(git describe --tags)
|
||||
mkdir $GITDESC
|
||||
echo "ARTDIR=$GITDESC" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@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
|
||||
38
.github/workflows/push-docker-image.yml
vendored
Normal file
38
.github/workflows/push-docker-image.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Push Docker Image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push-docker-image:
|
||||
name: Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix:
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Retrieve major version
|
||||
uses: winterjung/split@v2
|
||||
id: split
|
||||
with:
|
||||
msg: ${{ github.ref_name }}
|
||||
separator: .
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
- name: Build and push the Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ matrix.platforms }}
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_REPOSITORY }}:${{ github.ref_name }},${{ secrets.DOCKERHUB_REPOSITORY }}:${{ steps.split.outputs._0 }},${{ secrets.DOCKERHUB_REPOSITORY }}:latest
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -11,3 +11,10 @@ build/
|
||||
.vs/
|
||||
.idea/
|
||||
*.db
|
||||
*-shm
|
||||
*-wal
|
||||
version.h
|
||||
infer-out
|
||||
gmon.out
|
||||
*.bak
|
||||
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "tdata"]
|
||||
path = tdata
|
||||
url = https://github.com/OpenFusionProject/tabledata.git
|
||||
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug (Linux)",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/bin/fusion",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "Debug (Windows)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/bin/Debug/winfusion.exe",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "Release (Windows)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/bin/Release/winfusion.exe",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,9 @@ project(OpenFusion)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
|
||||
# OpenFusion supports multiple packet/struct versions
|
||||
# 104 is the default version to build which can be changed
|
||||
# For example: cmake -B build -DPROTOCOL_VERSION=728
|
||||
@@ -31,18 +34,28 @@ 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...
|
||||
if (NOT CMAKE_GENERATOR MATCHES "Visual Studio" AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND NOT CMAKE_GENERATOR MATCHES "MinGW Makefiles")
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(openfusion pthread)
|
||||
target_link_libraries(openfusion PRIVATE pthread)
|
||||
endif()
|
||||
|
||||
@@ -26,7 +26,7 @@ Both are pretty short reads and following them will get you up to speed with bra
|
||||
|
||||
I will now cover a few examples of the complications people have encountered contributing to this project, how to understand them, overcome them and henceforth avoid them.
|
||||
|
||||
## Dirty pull requests
|
||||
### Dirty pull requests
|
||||
|
||||
Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge.
|
||||
These are generally either:
|
||||
@@ -44,7 +44,7 @@ If you read the above links, you'll note that this isn't exactly a perfect solut
|
||||
The obvious issue, then, is that the people submitting dirty PRs are the exact people who don't *know* how to rebase their fork to continue submitting their work cleanly.
|
||||
So they end up creating countless merge commits when pulling upstream on top of their own incompatible histories, and then submitting those merge commits in their PRs and the cycle continues.
|
||||
|
||||
## The details
|
||||
### The details
|
||||
|
||||
A git commit is uniquely identified by its SHA1 hash.
|
||||
Its hash is generated from its contents, as well as the hash(es) of its parent commit(s).
|
||||
@@ -52,7 +52,7 @@ Its hash is generated from its contents, as well as the hash(es) of its parent c
|
||||
That means that even if two commits are exactly the same in terms of content, they're not the same commit if their parent commits differ (ex. if they've been rebased).
|
||||
So if you keep issuing `git pull`s after upstream has merged a rebased version of your PR, you will never re-synchronize with it, and will instead construct an alternate history polluted by pointless merge commits.
|
||||
|
||||
## The solution
|
||||
### The solution
|
||||
|
||||
If you already have a messed-up fork and you have no changes on it that you're afraid to lose, the solution is simple:
|
||||
|
||||
@@ -67,7 +67,7 @@ If you do have some committed changes that haven't yet been merged upstream, you
|
||||
If you do end up messing something up, don't worry, it most likely isn't really lost and `git reflog` is your friend.
|
||||
(You can checkout an arbitrary commit, and make it into its own branch with `git checkout -b BRANCH` or set a pre-exisitng branch to it with `git reset --hard COMMIT`)
|
||||
|
||||
## Avoiding the problem
|
||||
### Avoiding the problem
|
||||
|
||||
When working on a changeset you want to submit back upstream, don't do it on the main branch.
|
||||
Create a work branch just for your changeset with `git checkout -b work`.
|
||||
@@ -81,3 +81,34 @@ That way you can always keep master in sync with upstream with `git pull --ff-on
|
||||
Creating new branches for the rebase isn't strictly necessary since you can always return a branch to its previous state with `git reflog`.
|
||||
|
||||
For moving uncommited changes around between branches, `git stash` is a real blessing.
|
||||
|
||||
## Code guidelines
|
||||
|
||||
Alright, you're up to speed on Git and ready to go. Here are a few specific code guidelines to try and follow:
|
||||
|
||||
### Match the styling
|
||||
|
||||
Pretty straightforward, make sure your code looks similar to the code around it. Match whitespacing, bracket styling, variable naming conventions, etc.
|
||||
|
||||
### Prefer short-circuiting
|
||||
|
||||
To minimize branching complexity (as this makes the code hard to read), we prefer to keep the number of `if-else` statements as low as possible. One easy way to achieve this is by doing an early return after branching into an `if` block and then writing the code for the other path outside the block entirely. You can find examples of this in practically every source file. Note that in a few select situations, this might actually make your code less elegant, which is why this isn't a strict rule. Lean towards short-circuiting and use your better judgement.
|
||||
|
||||
### Follow the include convention
|
||||
|
||||
This one matters a lot as it can cause cyclic dependencies and other code-breaking issues.
|
||||
|
||||
FOR HEADER FILES (.hpp):
|
||||
- everything you use IN THE HEADER must be EXPLICITLY INCLUDED with the exception of things that fall under Core.hpp
|
||||
- you may NOT include ANYTHING ELSE
|
||||
|
||||
FOR SOURCE FILES (.cpp):
|
||||
- you can #include whatever you want as long as the partner header is included first
|
||||
- anything that gets included by another include is fair game
|
||||
- redundant includes are ok because they'll be harmless AS LONG AS our header files stay lean.
|
||||
|
||||
The point of this is NOT to optimize the number of includes used all around or make things more efficient necessarily. it's to improve readability & coherence and make it easier to avoid cyclical issues.
|
||||
|
||||
## When in doubt, ask
|
||||
|
||||
If you still have questions that were not answered here, feel free to ping a dev in the Discord server or on GitHub.
|
||||
|
||||
32
Dockerfile
Normal file
32
Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
# build
|
||||
FROM debian:stable-slim as build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get -y update && apt-get install -y \
|
||||
git \
|
||||
clang \
|
||||
make \
|
||||
libsqlite3-dev
|
||||
|
||||
COPY src ./src
|
||||
COPY vendor ./vendor
|
||||
COPY .git ./.git
|
||||
COPY Makefile CMakeLists.txt version.h.in ./
|
||||
|
||||
RUN make -j8
|
||||
|
||||
# prod
|
||||
FROM debian:stable-slim
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get -y update && apt-get install -y \
|
||||
libsqlite3-dev
|
||||
|
||||
COPY --from=build /usr/src/app/bin/fusion /bin/fusion
|
||||
COPY sql ./sql
|
||||
|
||||
CMD ["/bin/fusion"]
|
||||
|
||||
LABEL Name=openfusion Version=0.0.2
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Seth Stubbs
|
||||
Copyright (c) 2020-2024 OpenFusion Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
161
Makefile
161
Makefile
@@ -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/Buffs.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\
|
||||
src/Buffs.hpp\
|
||||
src/Chat.hpp\
|
||||
src/CustomCommands.hpp\
|
||||
src/Entities.hpp\
|
||||
src/Email.hpp\
|
||||
src/Eggs.hpp\
|
||||
src/Missions.hpp\
|
||||
src/MobAI.hpp\
|
||||
src/Combat.hpp\
|
||||
src/Nanos.hpp\
|
||||
src/Abilities.hpp\
|
||||
src/Items.hpp\
|
||||
src/NPCManager.hpp\
|
||||
src/Player.hpp\
|
||||
src/PlayerManager.hpp\
|
||||
src/PlayerMovement.hpp\
|
||||
src/BuiltinCommands.hpp\
|
||||
src/settings.hpp\
|
||||
src/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
|
||||
|
||||
109
README.md
109
README.md
@@ -1,30 +1,42 @@
|
||||

|
||||
<p align="center"><img width="640" src="res/openfusion-hero.png" alt=""></p>
|
||||
|
||||
[](https://ci.appveyor.com/project/OpenFusionProject/openfusion)
|
||||
[](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.5/OpenFusionClient-1.5-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.5/OpenFusionClient-1.5.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
|
||||
1. Grab `OpenFusionServer-1.5-Original.zip` or `OpenFusionServer-1.5-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.5).
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -41,42 +53,34 @@ FusionFall consists of the following components:
|
||||
|
||||
The original game made use of the player's actual web browser to launch the game, but since then the NPAPI plugin interface the game relied on has been deprecated and is no longer available in most modern browsers. Both Retro and OpenFusion get around this issue by distributing an older version of Electron, a software package that is essentially a specialized web browser.
|
||||
|
||||
The browser/Electron client opens a web page with an `<embed>` tag of MIME type `application/vnd.unity`, where the `src` param is the address of the game's `.unity3d` entrypoint.
|
||||
|
||||
This triggers the browser to load an NPAPI plugin that handles this MIME type, the Unity Web Player, which the browser looks for in `C:\Users\USERNAME\AppData\LocalLow\Unity\WebPlayer`.
|
||||
The Web Player was previously copied there by `installUnity.bat`.
|
||||
The browser/Electron client opens a web page with an `<embed>` tag of the appropriate MIME type, where the `src` param is the address of the game's `.unity3d` entrypoint. This triggers the browser to load an NPAPI plugin that handles said MIME type, in this case the Unity Web Player.
|
||||
|
||||
Note that the version of the web player distributed with OpenFusion expects a standard `UnityWeb` magic number for all assets, instead of Retro's modified `streamed` magic number.
|
||||
This will potentially become relevant later, as people start experimenting and mixing and matching versions.
|
||||
|
||||
The web player will execute the game code, which will request the following files from the server: `/assetInfo.php` and `/loginInfo.php`.
|
||||
|
||||
`FreeClient/resources/app/files/assetInfo.php` contains the address from which to fetch the rest of the game's assets (the "dongresources").
|
||||
`/assetInfo.php` contains the address from which to fetch the rest of the game's assets (the "dongresources").
|
||||
Normally those would be hosted on the same web server as the gateway, but the OpenFusion distribution (in it's default configuration) doesn't use a web server at all!
|
||||
It loads the web pages locally using the `file://` schema, and fetches the game's assets from Turner's CDN (which is still hosting them to this day!).
|
||||
It instead loads the web pages locally using the `file://` schema, and fetches the game's assets from a standard web server.
|
||||
|
||||
`FreeClient/resources/app/files/loginInfo.php` contains the IP:port pair of the FusionFall login server, which the client will connect to. This login server drives the client while it's in the Character Selection menu, as well as Character Creation and the Tutorial.
|
||||
`/loginInfo.php` contains the IP:port pair of the FusionFall login server, which the client will connect to. This login server drives the client while it's in the Character Selection menu, as well as Character Creation and the Tutorial.
|
||||
|
||||
When the player clicks "ENTER THE GAME" (or completes the tutorial), the login server sends it the address of the shard server, which the client will then connect to and remain connected to during gameplay.
|
||||
|
||||
## Configuration
|
||||
|
||||
You can change the ports the FusionFall server listens on in `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,37 +89,22 @@ 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.
|
||||
While most features are implemented and the game is playable start to finish, there may be missing functionality or bugs present.
|
||||
|
||||

|
||||
Depending on the server configuration, you'll have access to certain commands.
|
||||
|
||||
To make your landwalking experience more pleasant, you can make use of a few admin commands to get around easier:
|
||||
For the public servers: Original has item spawning, the ability to set player speed/jump height, and teleportation enabled (default account level 50).
|
||||
Meanwhile the Academy server is more meant for legitimate playthroughs (default account level 99).
|
||||
|
||||
### Movement commands
|
||||
* A `/speed` of around 2400 or 3000 is nice.
|
||||
* A `/jump` of about 50 will send you soaring
|
||||
* [This map](res/dong_number_map.png) (credit to Danny O) is useful for `/warp` coordinates.
|
||||
* `/goto` is useful for more precise teleportation (ie. for getting into Infected Zones, etc.).
|
||||
When hosting a local server, you will have access to all commands by default (account level 1).
|
||||
|
||||
### Item commands
|
||||
* `/itemN [type] [itemId] [amount]`
|
||||
(Refer to the [item list](https://docs.google.com/spreadsheets/d/1mpoJ9iTHl_xLI4wQ_9UvIDYNcsDYscdkyaGizs43TCg/))
|
||||
|
||||
### Nano commands
|
||||
* `/nano [id] (1-36)`
|
||||
* `/nano_equip [id] (1-36) [slot] (0-2)`
|
||||
* `/nano_unequip [slot] (0-2)`
|
||||
* `/nano_active [slot] (0-2)`
|
||||
|
||||
## 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.
|
||||
For a list of available commands, see [this wiki page](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).
|
||||
|
||||
79
appveyor.yml
79
appveyor.yml
@@ -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
|
||||
104
config.ini
104
config.ini
@@ -5,36 +5,98 @@
|
||||
# 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/
|
||||
|
||||
# Space-separated list of patch folders in patchdir to load from.
|
||||
# If you uncomment this, note that Academy builds *must* contain 1013,
|
||||
# and pre-Academy builds must *not* contain it.
|
||||
#enabledpatches=1013
|
||||
|
||||
# xdt json filename
|
||||
#xdtdata=xdt.json
|
||||
# NPC json filename
|
||||
#npcdata=NPCs.json
|
||||
# mob json filename
|
||||
#mobdata=mobs.json
|
||||
# path json filename
|
||||
#pathdata=paths.json
|
||||
# drop json filename
|
||||
#dropdata=drops.json
|
||||
# gruntwork output filename (this is what you submit)
|
||||
#gruntwork=gruntwork.json
|
||||
# location of the database
|
||||
#dbpath=database.db
|
||||
|
||||
# should there be a score cap for infected zone races?
|
||||
#izracescorecapped=true
|
||||
|
||||
# should tutorial flags be disabled off the bat?
|
||||
disablefirstuseflag=true
|
||||
|
||||
# account permission level that will be set upon character creation
|
||||
# 1 = default, will allow *all* commands
|
||||
# 30 = allow some more "abusable" commands such as /summon
|
||||
# 50 = only allow cheat commands, like /itemN and /speed
|
||||
# 99 = standard user account, no cheats allowed
|
||||
# any number higher than 50 will disable commands
|
||||
accountlevel=1
|
||||
|
||||
# should mobs drop event crates?
|
||||
# 0 = no event
|
||||
# 1 = Knishmas
|
||||
# 2 = Halloween
|
||||
# 3 = Easter
|
||||
eventmode=0
|
||||
|
||||
# you can override the default spawn point.
|
||||
# these example coords are for the Future (Z is height):
|
||||
#spawnx=632032
|
||||
#spawny=187177
|
||||
#spawnz=-5500
|
||||
|
||||
# Player location monitor interface configuration
|
||||
[monitor]
|
||||
enabled=false
|
||||
# the port to listen for connections on
|
||||
port=8003
|
||||
# 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
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
openfusion:
|
||||
image: openfusion
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
volumes:
|
||||
- ./config.ini:/usr/src/app/config.ini
|
||||
- ./database.db:/usr/src/app/database.db
|
||||
- ./tdata:/usr/src/app/tdata
|
||||
ports:
|
||||
- "23000:23000"
|
||||
- "23001:23001"
|
||||
- "8003:8003"
|
||||
BIN
res/openfusion-hero.png
Normal file
BIN
res/openfusion-hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 425 KiB |
18
sql/migration1.sql
Normal file
18
sql/migration1.sql
Normal 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
37
sql/migration2.sql
Normal 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
28
sql/migration3.sql
Normal 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
161
sql/tables.sql
Normal 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)
|
||||
)
|
||||
517
src/Abilities.cpp
Normal file
517
src/Abilities.cpp
Normal file
@@ -0,0 +1,517 @@
|
||||
#include "Abilities.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "NPCManager.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Buffs.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "MobAI.hpp"
|
||||
|
||||
using namespace Abilities;
|
||||
|
||||
std::map<int32_t, SkillData> Abilities::SkillTable;
|
||||
|
||||
#pragma region Skill handlers
|
||||
static SkillResult handleSkillDamage(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
EntityRef sourceRef = source->getRef();
|
||||
double scalingFactor = 1;
|
||||
if(sourceRef.kind == EntityKind::PLAYER)
|
||||
scalingFactor = std::max(source->getMaxHP(), target->getMaxHP()) / 1000.0;
|
||||
else
|
||||
scalingFactor = source->getMaxHP() / 1500.0;
|
||||
|
||||
int damage = (int)(skill->values[0][power] * scalingFactor);
|
||||
int dealt = target->takeDamage(sourceRef, damage);
|
||||
|
||||
sSkillResult_Damage result{};
|
||||
result.eCT = target->getCharType();
|
||||
result.iID = target->getID();
|
||||
result.bProtected = dealt <= 0;
|
||||
result.iDamage = dealt;
|
||||
result.iHP = target->getCurrentHP();
|
||||
return SkillResult(sizeof(sSkillResult_Damage), &result);
|
||||
}
|
||||
|
||||
static SkillResult handleSkillHealHP(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
EntityRef sourceRef = source->getRef();
|
||||
int heal = skill->values[0][power];
|
||||
double scalingFactor = target->getMaxHP() / 1000.0;
|
||||
int healed = target->heal(sourceRef, heal * scalingFactor);
|
||||
|
||||
sSkillResult_Heal_HP result{};
|
||||
result.eCT = target->getCharType();
|
||||
result.iID = target->getID();
|
||||
result.iHealHP = healed;
|
||||
result.iHP = target->getCurrentHP();
|
||||
return SkillResult(sizeof(sSkillResult_Heal_HP), &result);
|
||||
}
|
||||
|
||||
static SkillResult handleSkillDamageNDebuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
// take aggro
|
||||
target->takeDamage(source->getRef(), 0);
|
||||
|
||||
int duration = 0;
|
||||
int strength = 0;
|
||||
bool blocked = target->hasBuff(ECSB_FREEDOM);
|
||||
if(!blocked) {
|
||||
duration = skill->durationTime[power];
|
||||
strength = skill->values[0][power];
|
||||
BuffStack debuff = {
|
||||
(duration * 100) / MS_PER_COMBAT_TICK, // ticks
|
||||
strength, // value
|
||||
source->getRef(), // source
|
||||
BuffClass::NANO, // buff class
|
||||
};
|
||||
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
|
||||
target->addBuff(timeBuffId,
|
||||
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
|
||||
Buffs::timeBuffUpdate(self, buff, status, stack);
|
||||
if(status == ETBU_DEL) Buffs::timeBuffTimeout(self);
|
||||
},
|
||||
[](EntityRef self, Buff* buff, time_t currTime) {
|
||||
Buffs::timeBuffTick(self, buff);
|
||||
},
|
||||
&debuff);
|
||||
}
|
||||
|
||||
sSkillResult_Damage_N_Debuff result{};
|
||||
|
||||
result.iDamage = duration / 10; // we use the duration as the damage number (why?)
|
||||
result.iHP = target->getCurrentHP();
|
||||
result.eCT = target->getCharType();
|
||||
result.iID = target->getID();
|
||||
result.bProtected = blocked;
|
||||
result.iConditionBitFlag = target->getCompositeCondition();
|
||||
|
||||
// for player targets, make sure to update Nano stamina
|
||||
if (target->getCharType() == 1) {
|
||||
Player *plr = dynamic_cast<Player*>(target);
|
||||
result.iStamina = plr->getActiveNano()->iStamina;
|
||||
}
|
||||
|
||||
return SkillResult(sizeof(sSkillResult_Damage_N_Debuff), &result);
|
||||
}
|
||||
|
||||
static SkillResult handleSkillLeech(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
EntityRef sourceRef = source->getRef();
|
||||
int heal = skill->values[0][power];
|
||||
int healed = source->heal(sourceRef, heal);
|
||||
int damage = heal * 2;
|
||||
int dealt = target->takeDamage(sourceRef, damage);
|
||||
|
||||
sSkillResult_Leech result{};
|
||||
|
||||
result.Damage.eCT = target->getCharType();
|
||||
result.Damage.iID = target->getID();
|
||||
result.Damage.bProtected = dealt <= 0;
|
||||
result.Damage.iDamage = dealt;
|
||||
result.Damage.iHP = target->getCurrentHP();
|
||||
|
||||
result.Heal.eCT = result.Damage.eCT;
|
||||
result.Heal.iID = result.Damage.iID;
|
||||
result.Heal.iHealHP = healed;
|
||||
result.Heal.iHP = source->getCurrentHP();
|
||||
|
||||
return SkillResult(sizeof(sSkillResult_Leech), &result);
|
||||
}
|
||||
|
||||
static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
int duration = skill->durationTime[power];
|
||||
int strength = skill->values[0][power];
|
||||
BuffStack passiveBuff = {
|
||||
// if the duration is 0, it needs to be recast every tick
|
||||
duration == 0 ? 1 : (duration * 100) / MS_PER_COMBAT_TICK, // ticks
|
||||
strength, // value
|
||||
source->getRef(), // source
|
||||
source == target ? BuffClass::NANO : BuffClass::GROUP_NANO, // buff class
|
||||
};
|
||||
|
||||
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
|
||||
SkillDrainType drainType = skill->drainType;
|
||||
int combatLifetime = 0;
|
||||
if(!target->addBuff(timeBuffId,
|
||||
[drainType](EntityRef self, Buff* buff, int status, BuffStack* stack) {
|
||||
if(buff->id == ECSB_BOUNDINGBALL && status == ETBU_ADD) {
|
||||
// drain
|
||||
ICombatant* combatant = dynamic_cast<ICombatant*>(self.getEntity());
|
||||
combatant->takeDamage(buff->getLastSource(), 0); // aggro
|
||||
}
|
||||
Buffs::timeBuffUpdate(self, buff, status, stack);
|
||||
if(drainType == SkillDrainType::ACTIVE && status == ETBU_DEL)
|
||||
Buffs::timeBuffTimeout(self);
|
||||
},
|
||||
[combatLifetime](EntityRef self, Buff* buff, time_t currTime) mutable {
|
||||
if(buff->id == ECSB_BOUNDINGBALL &&
|
||||
combatLifetime % COMBAT_TICKS_PER_DRAIN_PROC == 0)
|
||||
Buffs::tickDrain(self, buff, COMBAT_TICKS_PER_DRAIN_PROC); // drain
|
||||
combatLifetime++;
|
||||
},
|
||||
&passiveBuff)) return SkillResult();
|
||||
|
||||
sSkillResult_Buff result{};
|
||||
result.eCT = target->getCharType();
|
||||
result.iID = target->getID();
|
||||
result.bProtected = false;
|
||||
result.iConditionBitFlag = target->getCompositeCondition();
|
||||
return SkillResult(sizeof(sSkillResult_Buff), &result);
|
||||
}
|
||||
|
||||
static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
if(target->getCharType() != 1)
|
||||
return SkillResult(); // only Players are valid targets for battery drain
|
||||
Player* plr = dynamic_cast<Player*>(target);
|
||||
|
||||
const double scalingFactor = (18 + source->getLevel()) / 36.0;
|
||||
const bool blocked = target->hasBuff(ECSB_PROTECT_BATTERY);
|
||||
|
||||
int boostDrain = 0;
|
||||
int potionDrain = 0;
|
||||
if(!blocked) {
|
||||
boostDrain = (int)(skill->values[0][power] * scalingFactor);
|
||||
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
|
||||
plr->batteryW -= boostDrain;
|
||||
|
||||
potionDrain = (int)(skill->values[1][power] * scalingFactor);
|
||||
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
|
||||
plr->batteryN -= potionDrain;
|
||||
}
|
||||
|
||||
sSkillResult_BatteryDrain result{};
|
||||
result.eCT = target->getCharType();
|
||||
result.iID = target->getID();
|
||||
result.bProtected = blocked;
|
||||
result.iDrainW = boostDrain;
|
||||
result.iBatteryW = plr->batteryW;
|
||||
result.iDrainN = potionDrain;
|
||||
result.iBatteryN = plr->batteryN;
|
||||
result.iStamina = plr->getActiveNano()->iStamina;
|
||||
result.bNanoDeactive = plr->getActiveNano()->iStamina <= 0;
|
||||
result.iConditionBitFlag = target->getCompositeCondition();
|
||||
return SkillResult(sizeof(sSkillResult_BatteryDrain), &result);
|
||||
}
|
||||
|
||||
static SkillResult handleSkillMove(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
if(source->getCharType() != 1)
|
||||
return SkillResult(); // only Players are valid sources for recall
|
||||
|
||||
Player* plr = dynamic_cast<Player*>(source);
|
||||
if(source == target) {
|
||||
// no trailing struct for self
|
||||
PlayerManager::sendPlayerTo(target->getRef().sock, plr->recallX, plr->recallY, plr->recallZ, plr->recallInstance);
|
||||
return SkillResult();
|
||||
}
|
||||
|
||||
sSkillResult_Move result{};
|
||||
result.eCT = target->getCharType();
|
||||
result.iID = target->getID();
|
||||
result.iMapNum = plr->recallInstance;
|
||||
result.iMoveX = plr->recallX;
|
||||
result.iMoveY = plr->recallY;
|
||||
result.iMoveZ = plr->recallZ;
|
||||
return SkillResult(sizeof(sSkillResult_Move), &result);
|
||||
}
|
||||
|
||||
static SkillResult handleSkillResurrect(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
sSkillResult_Resurrect result{};
|
||||
result.eCT = target->getCharType();
|
||||
result.iID = target->getID();
|
||||
result.iRegenHP = target->getCurrentHP();
|
||||
return SkillResult(sizeof(sSkillResult_Resurrect), &result);
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombatant* src, std::vector<ICombatant*> targets) {
|
||||
SkillResult (*skillHandler)(SkillData*, int, ICombatant*, ICombatant*) = nullptr;
|
||||
std::vector<SkillResult> results;
|
||||
|
||||
switch(skill->skillType)
|
||||
{
|
||||
case SkillType::CORRUPTIONATTACK:
|
||||
case SkillType::CORRUPTIONATTACKLOSE:
|
||||
case SkillType::CORRUPTIONATTACKWIN:
|
||||
// skillHandler = handleSkillCorruptionReflect;
|
||||
// break;
|
||||
case SkillType::DAMAGE:
|
||||
skillHandler = handleSkillDamage;
|
||||
break;
|
||||
case SkillType::HEAL_HP:
|
||||
case SkillType::RETURNHOMEHEAL:
|
||||
skillHandler = handleSkillHealHP;
|
||||
break;
|
||||
case SkillType::KNOCKDOWN:
|
||||
case SkillType::SLEEP:
|
||||
case SkillType::SNARE:
|
||||
case SkillType::STUN:
|
||||
skillHandler = handleSkillDamageNDebuff;
|
||||
break;
|
||||
case SkillType::JUMP:
|
||||
case SkillType::RUN:
|
||||
case SkillType::STEALTH:
|
||||
case SkillType::MINIMAPENEMY:
|
||||
case SkillType::MINIMAPTRESURE:
|
||||
case SkillType::PHOENIX:
|
||||
case SkillType::PROTECTBATTERY:
|
||||
case SkillType::PROTECTINFECTION:
|
||||
case SkillType::REWARDBLOB:
|
||||
case SkillType::REWARDCASH:
|
||||
// case SkillType::INFECTIONDAMAGE:
|
||||
case SkillType::FREEDOM:
|
||||
case SkillType::BOUNDINGBALL:
|
||||
case SkillType::INVULNERABLE:
|
||||
case SkillType::STAMINA_SELF:
|
||||
case SkillType::NANOSTIMPAK:
|
||||
case SkillType::BUFFHEAL:
|
||||
skillHandler = handleSkillBuff;
|
||||
break;
|
||||
case SkillType::BLOODSUCKING:
|
||||
skillHandler = handleSkillLeech;
|
||||
break;
|
||||
case SkillType::RETROROCKET_SELF:
|
||||
// no-op
|
||||
return results;
|
||||
case SkillType::PHOENIX_GROUP:
|
||||
skillHandler = handleSkillResurrect;
|
||||
break;
|
||||
case SkillType::RECALL:
|
||||
case SkillType::RECALL_GROUP:
|
||||
skillHandler = handleSkillMove;
|
||||
break;
|
||||
case SkillType::BATTERYDRAIN:
|
||||
skillHandler = handleSkillBatteryDrain;
|
||||
break;
|
||||
default:
|
||||
std::cout << "[WARN] Unhandled skill type " << (int)skill->skillType << std::endl;
|
||||
return results;
|
||||
}
|
||||
|
||||
for(ICombatant* target : targets) {
|
||||
assert(target != nullptr);
|
||||
SkillResult result = skillHandler(skill, power, src != nullptr ? src : target, target);
|
||||
if(result.size == 0) continue; // skill not applicable
|
||||
if(result.size > MAX_SKILLRESULT_SIZE) {
|
||||
std::cout << "[WARN] bad skill result size for " << (int)skill->skillType << " from " << (void*)handleSkillBuff << std::endl;
|
||||
continue;
|
||||
}
|
||||
results.push_back(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static void attachSkillResults(std::vector<SkillResult> results, uint8_t* pivot) {
|
||||
for(SkillResult& result : results) {
|
||||
size_t sz = result.size;
|
||||
memcpy(pivot, result.payload, sz);
|
||||
pivot += sz;
|
||||
}
|
||||
}
|
||||
|
||||
void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std::vector<ICombatant*> affected) {
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
ICombatant* combatant = dynamic_cast<ICombatant*>(plr);
|
||||
|
||||
int boost = 0;
|
||||
if (Nanos::getNanoBoost(plr))
|
||||
boost = 3;
|
||||
|
||||
if(skill->drainType == SkillDrainType::ACTIVE) {
|
||||
nano.iStamina -= skill->batteryUse[boost];
|
||||
if (nano.iStamina <= 0)
|
||||
nano.iStamina = 0;
|
||||
}
|
||||
|
||||
std::vector<SkillResult> results = handleSkill(skill, boost, combatant, affected);
|
||||
if(results.empty()) return; // no effect; no need for confirmation packets
|
||||
|
||||
// lazy validation since skill results might be different sizes
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), results.size(), MAX_SKILLRESULT_SIZE)) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_NANO_SKILL_USE_SUCC packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC);
|
||||
for(SkillResult& sr : results)
|
||||
resplen += sr.size;
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_NANO_SKILL_USE_SUCC* pkt = (sP_FE2CL_NANO_SKILL_USE_SUCC*)respbuf;
|
||||
pkt->iPC_ID = plr->iID;
|
||||
pkt->iNanoID = nano.iID;
|
||||
pkt->iSkillID = nano.iSkillID;
|
||||
pkt->iNanoStamina = nano.iStamina;
|
||||
pkt->bNanoDeactive = nano.iStamina <= 0;
|
||||
pkt->eST = (int32_t)skill->skillType;
|
||||
pkt->iTargetCnt = (int32_t)results.size();
|
||||
|
||||
attachSkillResults(results, (uint8_t*)(pkt + 1));
|
||||
sock->sendPacket(pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen);
|
||||
|
||||
if(skill->skillType == SkillType::RECALL_GROUP)
|
||||
// group recall packet is sent only to group members
|
||||
PlayerManager::sendToGroup(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen);
|
||||
else
|
||||
PlayerManager::sendToViewable(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen);
|
||||
}
|
||||
|
||||
void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*> affected) {
|
||||
if(SkillTable.count(skillID) == 0)
|
||||
return;
|
||||
|
||||
Entity* entity = npc.getEntity();
|
||||
ICombatant* src = nullptr;
|
||||
if(npc.kind == EntityKind::COMBAT_NPC || npc.kind == EntityKind::MOB)
|
||||
src = dynamic_cast<ICombatant*>(entity);
|
||||
|
||||
SkillData* skill = &SkillTable[skillID];
|
||||
|
||||
std::vector<SkillResult> results = handleSkill(skill, 0, src, affected);
|
||||
if(results.empty()) return; // no effect; no need for confirmation packets
|
||||
|
||||
// lazy validation since skill results might be different sizes
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), MAX_SKILLRESULT_SIZE)) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_HIT packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT);
|
||||
for(SkillResult& sr : results)
|
||||
resplen += sr.size;
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
|
||||
pkt->iNPC_ID = npc.id;
|
||||
pkt->iSkillID = skillID;
|
||||
pkt->eST = (int32_t)skill->skillType;
|
||||
pkt->iTargetCnt = (int32_t)results.size();
|
||||
if(npc.kind == EntityKind::MOB) {
|
||||
Mob* mob = dynamic_cast<Mob*>(entity);
|
||||
pkt->iValue1 = mob->hitX;
|
||||
pkt->iValue2 = mob->hitY;
|
||||
pkt->iValue3 = mob->hitZ;
|
||||
}
|
||||
|
||||
attachSkillResults(results, (uint8_t*)(pkt + 1));
|
||||
NPCManager::sendToViewable(entity, pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
|
||||
}
|
||||
|
||||
static std::vector<ICombatant*> entityRefsToCombatants(std::vector<EntityRef> refs) {
|
||||
std::vector<ICombatant*> combatants;
|
||||
for(EntityRef ref : refs) {
|
||||
if(ref.kind == EntityKind::PLAYER)
|
||||
combatants.push_back(dynamic_cast<ICombatant*>(PlayerManager::getPlayer(ref.sock)));
|
||||
else if(ref.kind == EntityKind::COMBAT_NPC || ref.kind == EntityKind::MOB)
|
||||
combatants.push_back(dynamic_cast<ICombatant*>(ref.getEntity()));
|
||||
}
|
||||
return combatants;
|
||||
}
|
||||
|
||||
std::vector<ICombatant*> Abilities::matchTargets(ICombatant* src, SkillData* skill, int count, int32_t *ids) {
|
||||
|
||||
if(skill->targetType == SkillTargetType::GROUP)
|
||||
return entityRefsToCombatants(src->getGroupMembers());
|
||||
|
||||
// this check *has* to happen after the group check above due to cases like group recall that use both
|
||||
if(skill->effectTarget == SkillEffectTarget::SELF)
|
||||
return {src}; // client sends 0 targets for certain self-targeting skills (recall)
|
||||
|
||||
// individuals
|
||||
std::vector<ICombatant*> targets;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int32_t id = ids[i];
|
||||
if (skill->targetType == SkillTargetType::MOBS) {
|
||||
// mob
|
||||
if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end()) {
|
||||
BaseNPC* npc = NPCManager::NPCs[id];
|
||||
if (npc->kind == EntityKind::COMBAT_NPC || npc->kind == EntityKind::MOB) {
|
||||
targets.push_back(dynamic_cast<ICombatant*>(npc));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
std::cout << "[WARN] skill: invalid mob target (id " << id << ")\n";
|
||||
} else if(skill->targetType == SkillTargetType::PLAYERS) {
|
||||
// player
|
||||
Player* plr = PlayerManager::getPlayerFromID(id);
|
||||
if (plr != nullptr) {
|
||||
targets.push_back(dynamic_cast<ICombatant*>(plr));
|
||||
continue;
|
||||
}
|
||||
std::cout << "[WARN] skill: invalid player target (id " << id << ")\n";
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/* ripped from client (enums emplaced) */
|
||||
int Abilities::getCSTBFromST(SkillType skillType) {
|
||||
int result = 0;
|
||||
switch (skillType)
|
||||
{
|
||||
case SkillType::RUN:
|
||||
result = ECSB_UP_MOVE_SPEED;
|
||||
break;
|
||||
case SkillType::JUMP:
|
||||
result = ECSB_UP_JUMP_HEIGHT;
|
||||
break;
|
||||
case SkillType::STEALTH:
|
||||
result = ECSB_UP_STEALTH;
|
||||
break;
|
||||
case SkillType::PHOENIX:
|
||||
result = ECSB_PHOENIX;
|
||||
break;
|
||||
case SkillType::PROTECTBATTERY:
|
||||
result = ECSB_PROTECT_BATTERY;
|
||||
break;
|
||||
case SkillType::PROTECTINFECTION:
|
||||
result = ECSB_PROTECT_INFECTION;
|
||||
break;
|
||||
case SkillType::MINIMAPENEMY:
|
||||
result = ECSB_MINIMAP_ENEMY;
|
||||
break;
|
||||
case SkillType::MINIMAPTRESURE:
|
||||
result = ECSB_MINIMAP_TRESURE;
|
||||
break;
|
||||
case SkillType::REWARDBLOB:
|
||||
result = ECSB_REWARD_BLOB;
|
||||
break;
|
||||
case SkillType::REWARDCASH:
|
||||
result = ECSB_REWARD_CASH;
|
||||
break;
|
||||
case SkillType::FREEDOM:
|
||||
result = ECSB_FREEDOM;
|
||||
break;
|
||||
case SkillType::INVULNERABLE:
|
||||
result = ECSB_INVULNERABLE;
|
||||
break;
|
||||
case SkillType::BUFFHEAL:
|
||||
result = ECSB_HEAL;
|
||||
break;
|
||||
case SkillType::NANOSTIMPAK:
|
||||
result = ECSB_STIMPAKSLOT1;
|
||||
// shift as necessary
|
||||
break;
|
||||
case SkillType::SNARE:
|
||||
result = ECSB_DN_MOVE_SPEED;
|
||||
break;
|
||||
case SkillType::STUN:
|
||||
result = ECSB_STUN;
|
||||
break;
|
||||
case SkillType::SLEEP:
|
||||
result = ECSB_MEZ;
|
||||
break;
|
||||
case SkillType::INFECTIONDAMAGE:
|
||||
result = ECSB_INFECTION;
|
||||
break;
|
||||
case SkillType::BOUNDINGBALL:
|
||||
result = ECSB_BOUNDINGBALL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
112
src/Abilities.hpp
Normal file
112
src/Abilities.hpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include "Entities.hpp"
|
||||
#include "Player.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <assert.h>
|
||||
|
||||
const int COMBAT_TICKS_PER_DRAIN_PROC = 2;
|
||||
constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain);
|
||||
|
||||
enum class SkillType {
|
||||
DAMAGE = 1,
|
||||
HEAL_HP = 2,
|
||||
KNOCKDOWN = 3, // uses DamageNDebuff
|
||||
SLEEP = 4, // uses DamageNDebuff
|
||||
SNARE = 5, // uses DamageNDebuff
|
||||
HEAL_STAMINA = 6,
|
||||
STAMINA_SELF = 7,
|
||||
STUN = 8, // uses DamageNDebuff
|
||||
WEAPONSLOW = 9,
|
||||
JUMP = 10,
|
||||
RUN = 11,
|
||||
STEALTH = 12,
|
||||
SWIM = 13,
|
||||
MINIMAPENEMY = 14,
|
||||
MINIMAPTRESURE = 15,
|
||||
PHOENIX = 16,
|
||||
PROTECTBATTERY = 17,
|
||||
PROTECTINFECTION = 18,
|
||||
REWARDBLOB = 19,
|
||||
REWARDCASH = 20,
|
||||
BATTERYDRAIN = 21,
|
||||
CORRUPTIONATTACK = 22,
|
||||
INFECTIONDAMAGE = 23,
|
||||
KNOCKBACK = 24,
|
||||
FREEDOM = 25,
|
||||
PHOENIX_GROUP = 26,
|
||||
RECALL = 27,
|
||||
RECALL_GROUP = 28,
|
||||
RETROROCKET_SELF = 29,
|
||||
BLOODSUCKING = 30,
|
||||
BOUNDINGBALL = 31,
|
||||
INVULNERABLE = 32,
|
||||
NANOSTIMPAK = 33,
|
||||
RETURNHOMEHEAL = 34,
|
||||
BUFFHEAL = 35,
|
||||
EXTRABANK = 36,
|
||||
CORRUPTIONATTACKWIN = 38,
|
||||
CORRUPTIONATTACKLOSE = 39,
|
||||
};
|
||||
|
||||
enum class SkillEffectTarget {
|
||||
POINT = 1,
|
||||
SELF = 2,
|
||||
CONE = 3,
|
||||
WEAPON = 4,
|
||||
AREA_SELF = 5,
|
||||
AREA_TARGET = 6
|
||||
};
|
||||
|
||||
enum class SkillTargetType {
|
||||
MOBS = 1,
|
||||
PLAYERS = 2,
|
||||
GROUP = 3
|
||||
};
|
||||
|
||||
enum class SkillDrainType {
|
||||
ACTIVE = 1,
|
||||
PASSIVE = 2
|
||||
};
|
||||
|
||||
struct SkillResult {
|
||||
size_t size;
|
||||
uint8_t payload[MAX_SKILLRESULT_SIZE];
|
||||
SkillResult(size_t len, void* dat) {
|
||||
assert(len <= MAX_SKILLRESULT_SIZE);
|
||||
size = len;
|
||||
memcpy(payload, dat, len);
|
||||
}
|
||||
SkillResult() {
|
||||
size = 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct SkillData {
|
||||
SkillType skillType; // eST
|
||||
SkillEffectTarget effectTarget;
|
||||
int effectType; // always 1?
|
||||
SkillTargetType targetType;
|
||||
SkillDrainType drainType;
|
||||
int effectArea;
|
||||
|
||||
int batteryUse[4];
|
||||
int durationTime[4];
|
||||
|
||||
int valueTypes[3];
|
||||
int values[3][4];
|
||||
};
|
||||
|
||||
namespace Abilities {
|
||||
extern std::map<int32_t, SkillData> SkillTable;
|
||||
|
||||
void useNanoSkill(CNSocket*, SkillData*, sNano&, std::vector<ICombatant*>);
|
||||
void useNPCSkill(EntityRef, int skillID, std::vector<ICombatant*>);
|
||||
|
||||
std::vector<ICombatant*> matchTargets(ICombatant*, SkillData*, int, int32_t*);
|
||||
int getCSTBFromST(SkillType skillType);
|
||||
}
|
||||
456
src/Buddies.cpp
Normal file
456
src/Buddies.cpp
Normal file
@@ -0,0 +1,456 @@
|
||||
#include "Buddies.hpp"
|
||||
|
||||
#include "db/Database.hpp"
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
|
||||
using namespace Buddies;
|
||||
|
||||
#pragma region Helper methods
|
||||
|
||||
static int getAvailableBuddySlot(Player* plr) {
|
||||
int slot = -1;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (plr->buddyIDs[i] == 0)
|
||||
return i;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
static bool playerHasBuddyWithID(Player* plr, int buddyID) {
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (plr->buddyIDs[i] == buddyID)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
|
||||
// Refresh buddy list
|
||||
void Buddies::sendBuddyList(CNSocket* sock) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
int buddyCnt = Database::getNumBuddies(plr);
|
||||
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC), buddyCnt, sizeof(sBuddyBaseInfo))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC) + buddyCnt * sizeof(sBuddyBaseInfo);
|
||||
uint8_t respbuf[CN_PACKET_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);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);
|
||||
|
||||
for (int slot = 0; slot < 50; slot++) {
|
||||
resp.aBuddyState[slot] = PlayerManager::getPlayerFromID(plr->buddyIDs[slot]) != nullptr ? 1 : 0;
|
||||
resp.aBuddyID[slot] = plr->buddyIDs[slot];
|
||||
}
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_GET_BUDDY_STATE_SUCC);
|
||||
}
|
||||
|
||||
// Blocking the buddy
|
||||
static void reqBuddyBlock(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_SET_BUDDY_BLOCK*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// sanity checks
|
||||
if (pkt->iBuddySlot < 0 || pkt->iBuddySlot >= 50 || plr->buddyIDs[pkt->iBuddySlot] != pkt->iBuddyPCUID)
|
||||
return;
|
||||
|
||||
// save in DB
|
||||
Database::removeBuddyship(plr->iID, pkt->iBuddyPCUID);
|
||||
Database::addBlock(plr->iID, pkt->iBuddyPCUID);
|
||||
|
||||
// save serverside
|
||||
// since ID is already in the array, just set it to blocked
|
||||
plr->isBuddyBlocked[pkt->iBuddySlot] = true;
|
||||
|
||||
// send response
|
||||
INITSTRUCT(sP_FE2CL_REP_SET_BUDDY_BLOCK_SUCC, resp);
|
||||
resp.iBuddyPCUID = pkt->iBuddyPCUID;
|
||||
resp.iBuddySlot = pkt->iBuddySlot;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_SET_BUDDY_BLOCK_SUCC);
|
||||
|
||||
// notify the other player he isn't a buddy anymore
|
||||
INITSTRUCT(sP_FE2CL_REP_REMOVE_BUDDY_SUCC, otherResp);
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
|
||||
if (otherSock == nullptr)
|
||||
return; // other player isn't online, no broadcast needed
|
||||
Player* otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
// search for the slot with the requesting player's ID
|
||||
otherResp.iBuddyPCUID = plr->PCStyle.iPC_UID;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (otherPlr->buddyIDs[i] == plr->PCStyle.iPC_UID) {
|
||||
// remove buddy
|
||||
otherPlr->buddyIDs[i] = 0;
|
||||
// broadcast
|
||||
otherResp.iBuddySlot = i;
|
||||
otherSock->sendPacket(otherResp, P_FE2CL_REP_REMOVE_BUDDY_SUCC);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// block non-buddy
|
||||
static void reqPlayerBlock(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_SET_PC_BLOCK*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
int buddySlot = getAvailableBuddySlot(plr);
|
||||
if (buddySlot == -1)
|
||||
return;
|
||||
|
||||
// save in DB
|
||||
Database::addBlock(plr->iID, pkt->iBlock_PCUID);
|
||||
|
||||
// save serverside
|
||||
plr->buddyIDs[buddySlot] = pkt->iBlock_PCUID;
|
||||
plr->isBuddyBlocked[buddySlot] = true;
|
||||
|
||||
// send response
|
||||
INITSTRUCT(sP_FE2CL_REP_SET_PC_BLOCK_SUCC, resp);
|
||||
resp.iBlock_ID = pkt->iBlock_ID;
|
||||
resp.iBlock_PCUID = pkt->iBlock_PCUID;
|
||||
resp.iBuddySlot = buddySlot;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_SET_PC_BLOCK_SUCC);
|
||||
}
|
||||
|
||||
// Deleting the buddy
|
||||
static void reqBuddyDelete(CNSocket* sock, CNPacketData* data) {
|
||||
// note! this packet is used both for removing buddies and blocks
|
||||
auto pkt = (sP_CL2FE_REQ_REMOVE_BUDDY*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// remove buddy on our side
|
||||
INITSTRUCT(sP_FE2CL_REP_REMOVE_BUDDY_SUCC, resp);
|
||||
resp.iBuddyPCUID = pkt->iBuddyPCUID;
|
||||
resp.iBuddySlot = pkt->iBuddySlot;
|
||||
if (pkt->iBuddySlot < 0 || pkt->iBuddySlot >= 50 || plr->buddyIDs[pkt->iBuddySlot] != pkt->iBuddyPCUID)
|
||||
return; // sanity check
|
||||
|
||||
bool wasBlocked = plr->isBuddyBlocked[resp.iBuddySlot];
|
||||
plr->buddyIDs[resp.iBuddySlot] = 0;
|
||||
plr->isBuddyBlocked[resp.iBuddySlot] = false;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_REMOVE_BUDDY_SUCC);
|
||||
|
||||
// remove record from db
|
||||
Database::removeBuddyship(plr->PCStyle.iPC_UID, pkt->iBuddyPCUID);
|
||||
// try this too
|
||||
Database::removeBlock(plr->PCStyle.iPC_UID, pkt->iBuddyPCUID);
|
||||
|
||||
if (wasBlocked)
|
||||
return;
|
||||
|
||||
// remove buddy on their side, reusing the struct
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
|
||||
if (otherSock == nullptr)
|
||||
return; // other player isn't online, no broadcast needed
|
||||
Player* otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
// search for the slot with the requesting player's ID
|
||||
resp.iBuddyPCUID = plr->PCStyle.iPC_UID;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (otherPlr->buddyIDs[i] == plr->PCStyle.iPC_UID) {
|
||||
// remove buddy
|
||||
otherPlr->buddyIDs[i] = 0;
|
||||
// broadcast
|
||||
resp.iBuddySlot = i;
|
||||
otherSock->sendPacket(resp, P_FE2CL_REP_REMOVE_BUDDY_SUCC);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warping to buddy
|
||||
static void reqBuddyWarp(CNSocket* sock, CNPacketData* data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto pkt = (sP_CL2FE_REQ_PC_BUDDY_WARP*)data->buf;
|
||||
|
||||
if (pkt->iSlotNum < 0 || pkt->iSlotNum >= 50)
|
||||
return; // sanity check
|
||||
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(pkt->iBuddyPCUID);
|
||||
if (otherPlr == nullptr)
|
||||
return; // buddy offline
|
||||
|
||||
// if the player is instanced; no warp allowed
|
||||
if (otherPlr->instanceID != INSTANCE_OVERWORLD)
|
||||
goto fail;
|
||||
|
||||
// check if the players are at the same point in time (or in the training area or not)
|
||||
if (otherPlr->PCStyle2.iPayzoneFlag != plr->PCStyle2.iPayzoneFlag)
|
||||
goto fail;
|
||||
|
||||
// do not warp to players on monkeys
|
||||
if (otherPlr->onMonkey)
|
||||
goto fail;
|
||||
|
||||
// does the player disallow warping?
|
||||
if (otherPlr->unwarpable)
|
||||
goto fail;
|
||||
|
||||
// otherPlr->instanceID should always be INSTANCE_OVERWORLD at this point
|
||||
PlayerManager::sendPlayerTo(sock, otherPlr->x, otherPlr->y, otherPlr->z, otherPlr->instanceID);
|
||||
return;
|
||||
|
||||
fail:
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_BUDDY_WARP_FAIL, resp);
|
||||
resp.iBuddyPCUID = pkt->iBuddyPCUID;
|
||||
resp.iErrorCode = 0;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_BUDDY_WARP_FAIL);
|
||||
}
|
||||
|
||||
void Buddies::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REQUEST_MAKE_BUDDY, requestBuddy);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY, reqBuddyByName);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ACCEPT_MAKE_BUDDY, reqAcceptBuddy);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY, reqFindNameBuddyAccept);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_GET_BUDDY_STATE, reqPktGetBuddyState);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SET_BUDDY_BLOCK, reqBuddyBlock);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SET_PC_BLOCK, reqPlayerBlock);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REMOVE_BUDDY, reqBuddyDelete);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BUDDY_WARP, reqBuddyWarp);
|
||||
}
|
||||
10
src/Buddies.hpp
Normal file
10
src/Buddies.hpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
namespace Buddies {
|
||||
void init();
|
||||
|
||||
// Buddy list
|
||||
void sendBuddyList(CNSocket* sock);
|
||||
}
|
||||
198
src/Buffs.cpp
Normal file
198
src/Buffs.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "Buffs.hpp"
|
||||
|
||||
#include "PlayerManager.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
|
||||
using namespace Buffs;
|
||||
|
||||
void Buff::tick(time_t currTime) {
|
||||
auto it = stacks.begin();
|
||||
while(it != stacks.end()) {
|
||||
BuffStack& stack = *it;
|
||||
//if(onTick) onTick(self, this, currTime);
|
||||
|
||||
if(stack.durationTicks == 0) {
|
||||
BuffStack deadStack = stack;
|
||||
it = stacks.erase(it);
|
||||
if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack);
|
||||
} else {
|
||||
if(stack.durationTicks > 0) stack.durationTicks--;
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Buff::combatTick(time_t currTime) {
|
||||
if(onCombatTick) onCombatTick(self, this, currTime);
|
||||
}
|
||||
|
||||
void Buff::clear() {
|
||||
while(!stacks.empty()) {
|
||||
BuffStack stack = stacks.back();
|
||||
stacks.pop_back();
|
||||
if(onUpdate) onUpdate(self, this, ETBU_DEL, &stack);
|
||||
}
|
||||
}
|
||||
|
||||
void Buff::clear(BuffClass buffClass) {
|
||||
auto it = stacks.begin();
|
||||
while(it != stacks.end()) {
|
||||
BuffStack& stack = *it;
|
||||
if(stack.buffStackClass == buffClass) {
|
||||
BuffStack deadStack = stack;
|
||||
it = stacks.erase(it);
|
||||
if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack);
|
||||
} else it++;
|
||||
}
|
||||
}
|
||||
|
||||
void Buff::addStack(BuffStack* stack) {
|
||||
stacks.push_back(*stack);
|
||||
if(onUpdate) onUpdate(self, this, ETBU_ADD, &stacks.back());
|
||||
}
|
||||
|
||||
bool Buff::hasClass(BuffClass buffClass) {
|
||||
for(BuffStack& stack : stacks) {
|
||||
if(stack.buffStackClass == buffClass)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
BuffClass Buff::maxClass() {
|
||||
BuffClass buffClass = BuffClass::NONE;
|
||||
for(BuffStack& stack : stacks) {
|
||||
if(stack.buffStackClass > buffClass)
|
||||
buffClass = stack.buffStackClass;
|
||||
}
|
||||
return buffClass;
|
||||
}
|
||||
|
||||
int Buff::getValue(BuffValueSelector selector) {
|
||||
if(isStale()) return 0;
|
||||
|
||||
int value = selector == BuffValueSelector::NET_TOTAL ? 0 : stacks.front().value;
|
||||
for(BuffStack& stack : stacks) {
|
||||
switch(selector)
|
||||
{
|
||||
case BuffValueSelector::NET_TOTAL:
|
||||
value += stack.value;
|
||||
break;
|
||||
case BuffValueSelector::MIN_VALUE:
|
||||
if(stack.value < value) value = stack.value;
|
||||
break;
|
||||
case BuffValueSelector::MAX_VALUE:
|
||||
if(stack.value > value) value = stack.value;
|
||||
break;
|
||||
case BuffValueSelector::MIN_MAGNITUDE:
|
||||
if(abs(stack.value) < abs(value)) value = stack.value;
|
||||
break;
|
||||
case BuffValueSelector::MAX_MAGNITUDE:
|
||||
default:
|
||||
if(abs(stack.value) > abs(value)) value = stack.value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
EntityRef Buff::getLastSource() {
|
||||
if(stacks.empty())
|
||||
return self;
|
||||
return stacks.back().source;
|
||||
}
|
||||
|
||||
bool Buff::isStale() {
|
||||
return stacks.empty();
|
||||
}
|
||||
|
||||
/* This will practically never do anything important, but it's here just in case */
|
||||
void Buff::updateCallbacks(BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fOnCombatTick) {
|
||||
if(!onUpdate) onUpdate = fOnUpdate;
|
||||
if(!onCombatTick) onCombatTick = fOnCombatTick;
|
||||
}
|
||||
|
||||
#pragma region Handlers
|
||||
void Buffs::timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack) {
|
||||
|
||||
if(self.kind != EntityKind::PLAYER)
|
||||
return; // not implemented
|
||||
|
||||
Player* plr = (Player*)self.getEntity();
|
||||
if(plr == nullptr)
|
||||
return; // sanity check
|
||||
|
||||
if(status == ETBU_DEL && !buff->isStale())
|
||||
return; // no premature effect deletion
|
||||
|
||||
int cbf = plr->getCompositeCondition();
|
||||
sTimeBuff payload{};
|
||||
if(status == ETBU_ADD) {
|
||||
payload.iValue = buff->getValue(BuffValueSelector::MAX_MAGNITUDE);
|
||||
// we need to explicitly add the ECSB for this buff,
|
||||
// in case this is the first stack in and the entry
|
||||
// in the buff map doesn't yet exist
|
||||
if(buff->id > 0) cbf |= CSB_FROM_ECSB(buff->id);
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
|
||||
pkt.eCSTB = buff->id; // eCharStatusTimeBuffID
|
||||
pkt.eTBU = status; // eTimeBuffUpdate
|
||||
pkt.eTBT = (int)stack->buffStackClass;
|
||||
pkt.iConditionBitFlag = cbf;
|
||||
pkt.TimeBuff = payload;
|
||||
self.sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
|
||||
}
|
||||
|
||||
void Buffs::timeBuffTick(EntityRef self, Buff* buff) {
|
||||
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
|
||||
return; // not implemented
|
||||
Entity* entity = self.getEntity();
|
||||
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK, pkt);
|
||||
pkt.eCT = combatant->getCharType();
|
||||
pkt.iID = combatant->getID();
|
||||
pkt.iTB_ID = buff->id;
|
||||
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
||||
}
|
||||
|
||||
void Buffs::timeBuffTimeout(EntityRef self) {
|
||||
if(self.kind != EntityKind::PLAYER && self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
|
||||
return; // not a combatant
|
||||
Entity* entity = self.getEntity();
|
||||
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt); // send a buff timeout to other players
|
||||
int32_t eCharType = combatant->getCharType();
|
||||
pkt.eCT = eCharType == 4 ? 2 : eCharType; // convention not followed by client here
|
||||
pkt.iID = combatant->getID();
|
||||
pkt.iConditionBitFlag = combatant->getCompositeCondition();
|
||||
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
||||
}
|
||||
|
||||
void Buffs::tickDrain(EntityRef self, Buff* buff, int mult) {
|
||||
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
|
||||
return; // not implemented
|
||||
Entity* entity = self.getEntity();
|
||||
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
|
||||
int damage = combatant->getMaxHP() / 100 * mult;
|
||||
int dealt = combatant->takeDamage(buff->getLastSource(), damage);
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
|
||||
pkt->iID = self.id;
|
||||
pkt->eCT = combatant->getCharType();
|
||||
pkt->iTB_ID = ECSB_BOUNDINGBALL;
|
||||
|
||||
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
||||
drain->iDamage = dealt;
|
||||
drain->iHP = combatant->getCurrentHP();
|
||||
drain->eCT = pkt->eCT;
|
||||
drain->iID = pkt->iID;
|
||||
|
||||
NPCManager::sendToViewable(self.getEntity(), (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
|
||||
}
|
||||
#pragma endregion
|
||||
93
src/Buffs.hpp
Normal file
93
src/Buffs.hpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include "EntityRef.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
/* forward declaration(s) */
|
||||
class Buff;
|
||||
template<class... Types>
|
||||
using BuffCallback = std::function<void(EntityRef, Buff*, Types...)>;
|
||||
|
||||
#define CSB_FROM_ECSB(x) (1 << (x - 1))
|
||||
|
||||
enum class BuffClass {
|
||||
NONE = ETBT_NONE,
|
||||
NANO = ETBT_NANO,
|
||||
GROUP_NANO = ETBT_GROUPNANO,
|
||||
EGG = ETBT_SHINY,
|
||||
ENVIRONMENT = ETBT_LANDEFFECT,
|
||||
ITEM = ETBT_ITEM,
|
||||
CASH_ITEM = ETBT_CASHITEM
|
||||
};
|
||||
|
||||
enum class BuffValueSelector {
|
||||
MAX_VALUE,
|
||||
MIN_VALUE,
|
||||
MAX_MAGNITUDE,
|
||||
MIN_MAGNITUDE,
|
||||
NET_TOTAL
|
||||
};
|
||||
|
||||
struct BuffStack {
|
||||
int durationTicks;
|
||||
int value;
|
||||
EntityRef source;
|
||||
BuffClass buffStackClass;
|
||||
};
|
||||
|
||||
class Buff {
|
||||
private:
|
||||
EntityRef self;
|
||||
std::vector<BuffStack> stacks;
|
||||
|
||||
public:
|
||||
int id;
|
||||
/* called just after a stack is added or removed */
|
||||
BuffCallback<int, BuffStack*> onUpdate;
|
||||
/* called when the buff is combat-ticked */
|
||||
BuffCallback<time_t> onCombatTick;
|
||||
|
||||
void tick(time_t);
|
||||
void combatTick(time_t);
|
||||
void clear();
|
||||
void clear(BuffClass buffClass);
|
||||
void addStack(BuffStack* stack);
|
||||
|
||||
/*
|
||||
* Sometimes we need to determine if a buff
|
||||
* is covered by a certain class, ex: nano
|
||||
* vs. coco egg in the case of infection protection
|
||||
*/
|
||||
bool hasClass(BuffClass buffClass);
|
||||
BuffClass maxClass();
|
||||
|
||||
int getValue(BuffValueSelector selector);
|
||||
EntityRef getLastSource();
|
||||
|
||||
/*
|
||||
* In general, a Buff object won't exist
|
||||
* unless it has stacks. However, when
|
||||
* popping stacks during iteration (onExpire),
|
||||
* stacks will be empty for a brief moment
|
||||
* when the last stack is popped.
|
||||
*/
|
||||
bool isStale();
|
||||
|
||||
void updateCallbacks(BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fonTick);
|
||||
|
||||
Buff(int iid, EntityRef pSelf, BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fOnCombatTick, BuffStack* firstStack)
|
||||
: self(pSelf), id(iid), onUpdate(fOnUpdate), onCombatTick(fOnCombatTick) {
|
||||
addStack(firstStack);
|
||||
}
|
||||
};
|
||||
|
||||
namespace Buffs {
|
||||
void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack);
|
||||
void timeBuffTick(EntityRef self, Buff* buff);
|
||||
void timeBuffTimeout(EntityRef self);
|
||||
void tickDrain(EntityRef self, Buff* buff, int mult);
|
||||
}
|
||||
379
src/BuiltinCommands.cpp
Normal file
379
src/BuiltinCommands.cpp
Normal file
@@ -0,0 +1,379 @@
|
||||
#include "BuiltinCommands.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Chat.hpp"
|
||||
#include "Nanos.hpp"
|
||||
|
||||
// helper function, not a packet handler
|
||||
void BuiltinCommands::setSpecialState(CNSocket* sock, CNPacketData* data) {
|
||||
auto setData = (sP_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH*)data->buf;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// HACK: work around the invisible weapon bug
|
||||
if (setData->iSpecialStateFlag == CN_SPECIAL_STATE_FLAG__FULL_UI)
|
||||
Items::updateEquips(sock, plr);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_SPECIAL_STATE_CHANGE, response);
|
||||
|
||||
plr->iSpecialState ^= setData->iSpecialStateFlag;
|
||||
|
||||
response.iPC_ID = setData->iPC_ID;
|
||||
response.iReqSpecialStateFlag = setData->iSpecialStateFlag;
|
||||
response.iSpecialState = plr->iSpecialState;
|
||||
|
||||
sock->sendPacket(response, P_FE2CL_REP_PC_SPECIAL_STATE_SWITCH_SUCC);
|
||||
PlayerManager::sendToViewable(sock, response, P_FE2CL_PC_SPECIAL_STATE_CHANGE);
|
||||
}
|
||||
|
||||
static void setGMSpecialSwitchPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
if (PlayerManager::getPlayer(sock)->accountLevel > 30)
|
||||
return;
|
||||
|
||||
BuiltinCommands::setSpecialState(sock, data);
|
||||
}
|
||||
|
||||
static void gotoPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
if (plr->accountLevel > 50)
|
||||
return;
|
||||
|
||||
auto gotoData = (sP_CL2FE_REQ_PC_GOTO*)data->buf;
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "P_CL2FE_REQ_PC_GOTO:" << std::endl;
|
||||
std::cout << "\tX: " << gotoData->iToX << std::endl;
|
||||
std::cout << "\tY: " << gotoData->iToY << std::endl;
|
||||
std::cout << "\tZ: " << gotoData->iToZ << std::endl;
|
||||
)
|
||||
|
||||
PlayerManager::sendPlayerTo(sock, gotoData->iToX, gotoData->iToY, gotoData->iToZ, INSTANCE_OVERWORLD);
|
||||
}
|
||||
|
||||
static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
if (plr->accountLevel > 50)
|
||||
return;
|
||||
|
||||
auto setData = (sP_CL2FE_GM_REQ_PC_SET_VALUE*)data->buf;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_SET_VALUE, response);
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "P_CL2FE_GM_REQ_PC_SET_VALUE:" << std::endl;
|
||||
std::cout << "\tPC_ID: " << setData->iPC_ID << std::endl;
|
||||
std::cout << "\tSetValueType: " << setData->iSetValueType << std::endl;
|
||||
std::cout << "\tSetValue: " << setData->iSetValue << std::endl;
|
||||
)
|
||||
|
||||
// Handle serverside value-changes
|
||||
switch (setData->iSetValueType) {
|
||||
case CN_GM_SET_VALUE_TYPE__HP:
|
||||
response.iSetValue = plr->HP = setData->iSetValue;
|
||||
break;
|
||||
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY :
|
||||
plr->batteryW = setData->iSetValue;
|
||||
|
||||
// caps
|
||||
if (plr->batteryW > 9999)
|
||||
plr->batteryW = 9999;
|
||||
|
||||
response.iSetValue = plr->batteryW;
|
||||
break;
|
||||
case CN_GM_SET_VALUE_TYPE__NANO_BATTERY:
|
||||
plr->batteryN = setData->iSetValue;
|
||||
|
||||
// caps
|
||||
if (plr->batteryN > 9999)
|
||||
plr->batteryN = 9999;
|
||||
|
||||
response.iSetValue = plr->batteryN;
|
||||
break;
|
||||
case CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
|
||||
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
|
||||
response.iSetValue = plr->fusionmatter;
|
||||
break;
|
||||
case CN_GM_SET_VALUE_TYPE__CANDY:
|
||||
response.iSetValue = plr->money = setData->iSetValue;
|
||||
break;
|
||||
case CN_GM_SET_VALUE_TYPE__SPEED:
|
||||
case CN_GM_SET_VALUE_TYPE__JUMP:
|
||||
response.iSetValue = setData->iSetValue;
|
||||
break;
|
||||
}
|
||||
|
||||
response.iPC_ID = setData->iPC_ID;
|
||||
response.iSetValueType = setData->iSetValueType;
|
||||
|
||||
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);
|
||||
|
||||
// if one lowers their own health to 0, make sure others can see it
|
||||
if (plr->HP <= 0) {
|
||||
INITSTRUCT(sP_FE2CL_PC_SUDDEN_DEAD, dead);
|
||||
|
||||
dead.iPC_ID = plr->iID;
|
||||
dead.iDamage = plr->HP;
|
||||
dead.iHP = plr->HP = 0;
|
||||
|
||||
PlayerManager::sendToViewable(sock, dead, P_FE2CL_PC_SUDDEN_DEAD);
|
||||
}
|
||||
}
|
||||
|
||||
static void setGMSpecialOnOff(CNSocket *sock, CNPacketData *data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// access check
|
||||
if (plr->accountLevel > 30)
|
||||
return;
|
||||
|
||||
auto req = (sP_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF*)data->buf;
|
||||
|
||||
CNSocket *otherSock = PlayerManager::getSockFromAny(req->eTargetSearchBy, req->iTargetPC_ID, req->iTargetPC_UID,
|
||||
AUTOU16TOU8(req->szTargetPC_FirstName), AUTOU16TOU8(req->szTargetPC_LastName));
|
||||
if (otherSock == nullptr) {
|
||||
Chat::sendServerMessage(sock, "player not found");
|
||||
return;
|
||||
}
|
||||
|
||||
Player *otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
if (req->iONOFF)
|
||||
otherPlr->iSpecialState |= req->iSpecialStateFlag;
|
||||
else
|
||||
otherPlr->iSpecialState &= ~req->iSpecialStateFlag;
|
||||
|
||||
// this is only used for muting players, so no need to update the client since that logic is server-side
|
||||
}
|
||||
|
||||
static void locatePlayer(CNSocket *sock, CNPacketData *data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// access check
|
||||
if (plr->accountLevel > 30)
|
||||
return;
|
||||
|
||||
auto req = (sP_CL2FE_GM_REQ_PC_LOCATION*)data->buf;
|
||||
|
||||
CNSocket *otherSock = PlayerManager::getSockFromAny(req->eTargetSearchBy, req->iTargetPC_ID, req->iTargetPC_UID,
|
||||
AUTOU16TOU8(req->szTargetPC_FirstName), AUTOU16TOU8(req->szTargetPC_LastName));
|
||||
if (otherSock == nullptr) {
|
||||
Chat::sendServerMessage(sock, "player not found");
|
||||
return;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_LOCATION, resp);
|
||||
Player *otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
resp.iTargetPC_UID = otherPlr->accountId;
|
||||
resp.iTargetPC_ID = otherPlr->iID;
|
||||
resp.iShardID = 0; // sharding is unsupported
|
||||
resp.iMapType = !!PLAYERID(otherPlr->instanceID); // private instance or not
|
||||
resp.iMapID = PLAYERID(otherPlr->instanceID);
|
||||
resp.iMapNum = MAPNUM(otherPlr->instanceID);
|
||||
resp.iX = otherPlr->x;
|
||||
resp.iY = otherPlr->y;
|
||||
resp.iZ = otherPlr->z;
|
||||
|
||||
memcpy(resp.szTargetPC_FirstName, otherPlr->PCStyle.szFirstName, sizeof(resp.szTargetPC_FirstName));
|
||||
memcpy(resp.szTargetPC_LastName, otherPlr->PCStyle.szLastName, sizeof(resp.szTargetPC_LastName));
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_GM_REP_PC_LOCATION);
|
||||
}
|
||||
|
||||
static void kickPlayer(CNSocket *sock, CNPacketData *data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// access check
|
||||
if (plr->accountLevel > 30)
|
||||
return;
|
||||
|
||||
auto req = (sP_CL2FE_GM_REQ_KICK_PLAYER*)data->buf;
|
||||
|
||||
CNSocket *otherSock = PlayerManager::getSockFromAny(req->eTargetSearchBy, req->iTargetPC_ID, req->iTargetPC_UID,
|
||||
AUTOU16TOU8(req->szTargetPC_FirstName), AUTOU16TOU8(req->szTargetPC_LastName));
|
||||
if (otherSock == nullptr) {
|
||||
Chat::sendServerMessage(sock, "player not found");
|
||||
return;
|
||||
}
|
||||
|
||||
Player *otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
if (plr->accountLevel > otherPlr->accountLevel) {
|
||||
Chat::sendServerMessage(sock, "player has higher access level");
|
||||
return;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_EXIT_SUCC, response);
|
||||
|
||||
response.iID = otherPlr->iID;
|
||||
response.iExitCode = 3; // "a GM has terminated your connection"
|
||||
|
||||
// send to target player
|
||||
otherSock->sendPacket(response, P_FE2CL_REP_PC_EXIT_SUCC);
|
||||
|
||||
// ensure that the connection has terminated
|
||||
otherSock->kill();
|
||||
}
|
||||
|
||||
static void warpToPlayer(CNSocket *sock, CNPacketData *data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// access check
|
||||
if (plr->accountLevel > 30)
|
||||
return;
|
||||
|
||||
auto req = (sP_CL2FE_REQ_PC_WARP_TO_PC*)data->buf;
|
||||
|
||||
Player *otherPlr = PlayerManager::getPlayerFromID(req->iPC_ID);
|
||||
if (otherPlr == nullptr) {
|
||||
Chat::sendServerMessage(sock, "player not found");
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerManager::sendPlayerTo(sock, otherPlr->x, otherPlr->y, otherPlr->z, otherPlr->instanceID);
|
||||
}
|
||||
|
||||
// GM teleport command
|
||||
static void teleportPlayer(CNSocket *sock, CNPacketData *data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// access check
|
||||
if (plr->accountLevel > 30)
|
||||
return;
|
||||
|
||||
auto req = (sP_CL2FE_GM_REQ_TARGET_PC_TELEPORT*)data->buf;
|
||||
|
||||
// player to teleport
|
||||
CNSocket *targetSock = PlayerManager::getSockFromAny(req->eTargetPCSearchBy, req->iTargetPC_ID, req->iTargetPC_UID,
|
||||
AUTOU16TOU8(req->szTargetPC_FirstName), AUTOU16TOU8(req->szTargetPC_LastName));
|
||||
if (targetSock == nullptr) {
|
||||
Chat::sendServerMessage(sock, "player to teleport not found");
|
||||
return;
|
||||
}
|
||||
|
||||
CNSocket *goalSock = nullptr;
|
||||
Player *goalPlr = nullptr;
|
||||
Player *targetPlr = nullptr;
|
||||
uint64_t instance = plr->instanceID;
|
||||
const int unstickRange = 400;
|
||||
|
||||
switch ((eCN_GM_TeleportType)req->eTeleportType) {
|
||||
case eCN_GM_TeleportType::MyLocation:
|
||||
PlayerManager::sendPlayerTo(targetSock, plr->x, plr->y, plr->z, instance);
|
||||
break;
|
||||
case eCN_GM_TeleportType::MapXYZ:
|
||||
instance = req->iToMap;
|
||||
// fallthrough
|
||||
case eCN_GM_TeleportType::XYZ:
|
||||
PlayerManager::sendPlayerTo(targetSock, req->iToX, req->iToY, req->iToZ, instance);
|
||||
break;
|
||||
case eCN_GM_TeleportType::SomeoneLocation:
|
||||
// player to teleport to
|
||||
goalSock = PlayerManager::getSockFromAny(req->eGoalPCSearchBy, req->iGoalPC_ID, req->iGoalPC_UID,
|
||||
AUTOU16TOU8(req->szGoalPC_FirstName), AUTOU16TOU8(req->szGoalPC_LastName));
|
||||
if (goalSock == nullptr) {
|
||||
Chat::sendServerMessage(sock, "teleportation target player not found");
|
||||
return;
|
||||
}
|
||||
goalPlr = PlayerManager::getPlayer(goalSock);
|
||||
|
||||
PlayerManager::sendPlayerTo(targetSock, goalPlr->x, goalPlr->y, goalPlr->z, goalPlr->instanceID);
|
||||
break;
|
||||
case eCN_GM_TeleportType::Unstick:
|
||||
targetPlr = PlayerManager::getPlayer(targetSock);
|
||||
|
||||
PlayerManager::sendPlayerTo(targetSock, targetPlr->x - unstickRange/2 + Rand::rand(unstickRange),
|
||||
targetPlr->y - unstickRange/2 + Rand::rand(unstickRange), targetPlr->z + 80);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void itemGMGiveHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto itemreq = (sP_CL2FE_REQ_PC_GIVE_ITEM*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (plr->accountLevel > 50) {
|
||||
// TODO: send fail packet
|
||||
return;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp);
|
||||
|
||||
if (itemreq->eIL == 1) {
|
||||
|
||||
if (Items::ItemData.find(std::pair<int32_t, int32_t>(itemreq->Item.iID, itemreq->Item.iType)) == Items::ItemData.end()
|
||||
|| itemreq->Item.iType < 0 || itemreq->Item.iType > 10) {
|
||||
// invalid item
|
||||
std::cout << "[WARN] Item id " << itemreq->Item.iID << " with type " << itemreq->Item.iType << " is invalid (give item)" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemreq->Item.iType == 10) {
|
||||
// item is vehicle, set expiration date
|
||||
// set time limit: current time + 7days
|
||||
itemreq->Item.iTimeLimit = getTimestamp() + 604800;
|
||||
}
|
||||
|
||||
plr->Inven[itemreq->iSlotNum] = itemreq->Item;
|
||||
} else if (itemreq->eIL == 2) {
|
||||
int id = itemreq->Item.iID;
|
||||
int slot = Missions::findQSlot(plr, id);
|
||||
|
||||
if (slot == -1) {
|
||||
std::cout << "[WARN] Player has no room for quest items" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (id != 0)
|
||||
std::cout << "new qitem in slot " << slot << std::endl;
|
||||
|
||||
// update player
|
||||
if (id != 0) {
|
||||
plr->QInven[slot].iType = 8;
|
||||
plr->QInven[slot].iID = id;
|
||||
plr->QInven[slot].iOpt += itemreq->Item.iOpt;
|
||||
|
||||
// destroy the item if its 0
|
||||
if (plr->QInven[slot].iOpt == 0)
|
||||
memset(&plr->QInven[slot], 0, sizeof(sItemBase));
|
||||
}
|
||||
std::cout << "Item id " << id << " is in slot " << slot << " of count " << plr->QInven[slot].iOpt << std::endl;
|
||||
}
|
||||
|
||||
resp.eIL = itemreq->eIL;
|
||||
resp.iSlotNum = itemreq->iSlotNum;
|
||||
resp.Item = itemreq->Item;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
|
||||
}
|
||||
|
||||
static void nanoGMGiveHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto nano = (sP_CL2FE_REQ_PC_GIVE_NANO*)data->buf;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (plr->accountLevel > 50)
|
||||
return;
|
||||
|
||||
// Add nano to player
|
||||
Nanos::addNano(sock, nano->iNanoID, 0);
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << PlayerManager::getPlayerName(plr) << " requested to add nano id: " << nano->iNanoID << std::endl;
|
||||
)
|
||||
}
|
||||
|
||||
void BuiltinCommands::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GOTO, gotoPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SET_VALUE, setValuePlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_ITEM, itemGMGiveHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_NANO, nanoGMGiveHandler);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH, setGMSpecialSwitchPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF, setGMSpecialOnOff);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_LOCATION, locatePlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_KICK_PLAYER, kickPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_TO_PC, warpToPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_TARGET_PC_TELEPORT, teleportPlayer);
|
||||
}
|
||||
9
src/BuiltinCommands.hpp
Normal file
9
src/BuiltinCommands.hpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
namespace BuiltinCommands {
|
||||
void init();
|
||||
|
||||
void setSpecialState(CNSocket *sock, CNPacketData *data);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
CNStructs.hpp - defines some basic structs & useful methods for packets used by FusionFall based on the version defined
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef _MSC_VER
|
||||
// codecvt_* is deprecated in C++17 and MSVC will throw an annoying warning because of that.
|
||||
// Defining this before anything else to silence it.
|
||||
#define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
|
||||
#endif
|
||||
|
||||
#include <iostream>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#ifndef _MSC_VER
|
||||
#include <sys/time.h>
|
||||
#else
|
||||
// Can't use this in MSVC.
|
||||
#include <time.h>
|
||||
#endif
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <locale>
|
||||
#include <codecvt>
|
||||
|
||||
// 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));
|
||||
|
||||
// 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();
|
||||
|
||||
// The PROTOCOL_VERSION definition is defined by the build system.
|
||||
#if !defined(PROTOCOL_VERSION)
|
||||
#include "structs/0104.hpp"
|
||||
#elif PROTOCOL_VERSION == 728
|
||||
#include "structs/0728.hpp"
|
||||
#elif PROTOCOL_VERSION == 104
|
||||
#include "structs/0104.hpp"
|
||||
#else
|
||||
#error Invalid PROTOCOL_VERSION
|
||||
#endif
|
||||
319
src/Chat.cpp
Normal file
319
src/Chat.cpp
Normal file
@@ -0,0 +1,319 @@
|
||||
#include "Chat.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "PlayerManager.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);
|
||||
|
||||
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;
|
||||
|
||||
if (plr->group == nullptr)
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
|
||||
else
|
||||
Groups::sendToGroup(plr->group, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
|
||||
}
|
||||
|
||||
static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
|
||||
std::string logLine = "[GroupMenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
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;
|
||||
|
||||
if (plr->group == nullptr)
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
|
||||
Groups::sendToGroup(plr->group, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
|
||||
}
|
||||
|
||||
// we only allow plain ascii, at least for now
|
||||
std::string Chat::sanitizeText(std::string text, bool allowNewlines) {
|
||||
int i;
|
||||
const int BUFSIZE = 512;
|
||||
char buf[BUFSIZE];
|
||||
|
||||
assert(text.size() < BUFSIZE);
|
||||
|
||||
i = 0;
|
||||
for (char c : text) {
|
||||
if (i >= BUFSIZE-1)
|
||||
break;
|
||||
|
||||
if (!allowNewlines && c == '\n')
|
||||
continue;
|
||||
|
||||
if ((c >= ' ' && c <= '~') || c == '\n')
|
||||
buf[i++] = c;
|
||||
}
|
||||
buf[i] = 0;
|
||||
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
void Chat::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_FREECHAT_MESSAGE, chatHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT, emoteHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE, menuChatHandler);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE, buddyChatHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE, buddyMenuChatHandler);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_EMOTES_CHAT, tradeChatHandler);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE, groupChatHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE, groupMenuChatHandler);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_ANNOUNCE, announcementHandler);
|
||||
}
|
||||
16
src/Chat.hpp
Normal file
16
src/Chat.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#define CMD_PREFIX '/'
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
344
src/Chunking.cpp
Normal file
344
src/Chunking.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
#include "Chunking.hpp"
|
||||
|
||||
#include "MobAI.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
using namespace Chunking;
|
||||
|
||||
/*
|
||||
* The initial chunkPos value before a player is placed into the world.
|
||||
*/
|
||||
const ChunkPos Chunking::INVALID_CHUNK = {};
|
||||
|
||||
std::map<ChunkPos, Chunk*> Chunking::chunks;
|
||||
|
||||
static void newChunk(ChunkPos pos) {
|
||||
if (chunkExists(pos)) {
|
||||
std::cout << "[WARN] Tried to create a chunk that already exists" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
Chunk *chunk = new Chunk();
|
||||
chunks[pos] = chunk;
|
||||
|
||||
// add the chunk to the cache of all players and NPCs in the surrounding chunks
|
||||
std::set<Chunk*> surroundings = getViewableChunks(pos);
|
||||
for (Chunk* c : surroundings)
|
||||
for (const EntityRef ref : c->entities)
|
||||
ref.getEntity()->viewableChunks.insert(chunk);
|
||||
}
|
||||
|
||||
static void deleteChunk(ChunkPos pos) {
|
||||
if (!chunkExists(pos)) {
|
||||
std::cout << "[WARN] Tried to delete a chunk that doesn't exist" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
Chunk* chunk = chunks[pos];
|
||||
|
||||
// remove the chunk from the cache of all players and NPCs in the surrounding chunks
|
||||
std::set<Chunk*> surroundings = getViewableChunks(pos);
|
||||
for(Chunk* c : surroundings)
|
||||
for (const EntityRef ref : c->entities)
|
||||
ref.getEntity()->viewableChunks.erase(chunk);
|
||||
|
||||
chunks.erase(pos); // remove from map
|
||||
delete chunk; // free from memory
|
||||
}
|
||||
|
||||
void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef ref) {
|
||||
if (!chunkExists(chunkPos))
|
||||
return; // shouldn't happen
|
||||
|
||||
chunks[chunkPos]->entities.insert(ref);
|
||||
|
||||
if (ref.kind == EntityKind::PLAYER)
|
||||
chunks[chunkPos]->nplayers++;
|
||||
}
|
||||
|
||||
void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) {
|
||||
if (!chunkExists(chunkPos))
|
||||
return; // do nothing if chunk doesn't even exist
|
||||
|
||||
Chunk* chunk = chunks[chunkPos];
|
||||
|
||||
chunk->entities.erase(ref); // gone
|
||||
|
||||
if (ref.kind == EntityKind::PLAYER)
|
||||
chunks[chunkPos]->nplayers--;
|
||||
assert(chunks[chunkPos]->nplayers >= 0);
|
||||
|
||||
// if chunk is completely empty, free it
|
||||
if (chunk->entities.size() == 0)
|
||||
deleteChunk(chunkPos);
|
||||
}
|
||||
|
||||
void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
|
||||
Entity *ent = ref.getEntity();
|
||||
bool alive = ent->isExtant();
|
||||
|
||||
// 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.kind == EntityKind::PLAYER) {
|
||||
ent->enterIntoViewOf(otherRef.sock);
|
||||
}
|
||||
|
||||
// notify this *player* of the existence of all visible Entities
|
||||
if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
|
||||
other->enterIntoViewOf(ref.sock);
|
||||
}
|
||||
|
||||
// for mobs, increment playersInView
|
||||
if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER)
|
||||
((Mob*)ent)->playersInView++;
|
||||
if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER)
|
||||
((Mob*)other)->playersInView++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref) {
|
||||
Entity *ent = ref.getEntity();
|
||||
bool alive = ent->isExtant();
|
||||
|
||||
// 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.kind == EntityKind::PLAYER) {
|
||||
ent->disappearFromViewOf(otherRef.sock);
|
||||
}
|
||||
|
||||
// notify this *player* of the departure of all visible Entities
|
||||
if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
|
||||
other->disappearFromViewOf(ref.sock);
|
||||
}
|
||||
|
||||
// for mobs, decrement playersInView
|
||||
if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER)
|
||||
((Mob*)ent)->playersInView--;
|
||||
if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER)
|
||||
((Mob*)other)->playersInView--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void emptyChunk(ChunkPos chunkPos) {
|
||||
if (!chunkExists(chunkPos)) {
|
||||
std::cout << "[WARN] Tried to empty chunk that doesn't exist\n";
|
||||
return; // chunk doesn't exist, we don't need to do anything
|
||||
}
|
||||
|
||||
Chunk* chunk = chunks[chunkPos];
|
||||
|
||||
if (chunk->nplayers > 0) {
|
||||
std::cout << "[WARN] Tried to empty chunk that still had players\n";
|
||||
return; // chunk doesn't exist, we don't need to do anything
|
||||
}
|
||||
|
||||
// unspawn all of the mobs/npcs
|
||||
std::set refs(chunk->entities);
|
||||
for (const EntityRef ref : refs) {
|
||||
if (ref.kind == EntityKind::PLAYER)
|
||||
assert(0);
|
||||
|
||||
// every call of this will check if the chunk is empty and delete it if so
|
||||
NPCManager::destroyNPC(ref.id);
|
||||
}
|
||||
}
|
||||
|
||||
void Chunking::updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to) {
|
||||
Entity* ent = ref.getEntity();
|
||||
|
||||
// move to other chunk's player set
|
||||
untrackEntity(from, ref); // this will delete the chunk if it's empty
|
||||
|
||||
// if the new chunk doesn't exist, make it first
|
||||
if (!chunkExists(to))
|
||||
newChunk(to);
|
||||
|
||||
trackEntity(to, ref);
|
||||
|
||||
// calculate viewable chunks from both points
|
||||
std::set<Chunk*> oldViewables = getViewableChunks(from);
|
||||
std::set<Chunk*> newViewables = getViewableChunks(to);
|
||||
std::set<Chunk*> toExit, toEnter;
|
||||
|
||||
/*
|
||||
* Calculate diffs. This is done to prevent phasing on chunk borders.
|
||||
* toExit will contain old viewables - new viewables, so the player will only be exited in chunks that are out of sight.
|
||||
* toEnter contains the opposite: new viewables - old viewables, chunks where we previously weren't visible from before.
|
||||
*/
|
||||
std::set_difference(oldViewables.begin(), oldViewables.end(), newViewables.begin(), newViewables.end(),
|
||||
std::inserter(toExit, toExit.end())); // chunks we must be EXITed from (old - new)
|
||||
std::set_difference(newViewables.begin(), newViewables.end(), oldViewables.begin(), oldViewables.end(),
|
||||
std::inserter(toEnter, toEnter.end())); // chunks we must be ENTERed into (new - old)
|
||||
|
||||
// update views
|
||||
removeEntityFromChunks(toExit, ref);
|
||||
addEntityToChunks(toEnter, ref);
|
||||
|
||||
ent->chunkPos = to; // update cached chunk position
|
||||
// updated cached viewable chunks
|
||||
ent->viewableChunks.clear();
|
||||
ent->viewableChunks.insert(newViewables.begin(), newViewables.end());
|
||||
}
|
||||
|
||||
bool Chunking::chunkExists(ChunkPos chunk) {
|
||||
return chunks.find(chunk) != chunks.end();
|
||||
}
|
||||
|
||||
ChunkPos Chunking::chunkPosAt(int posX, int posY, uint64_t instanceID) {
|
||||
return ChunkPos(posX / (settings::VIEWDISTANCE / 3), posY / (settings::VIEWDISTANCE / 3), instanceID);
|
||||
}
|
||||
|
||||
std::set<Chunk*> Chunking::getViewableChunks(ChunkPos chunk) {
|
||||
std::set<Chunk*> chnks;
|
||||
|
||||
int x, y;
|
||||
uint64_t inst;
|
||||
std::tie(x, y, inst) = chunk;
|
||||
|
||||
// grabs surrounding chunks if they exist
|
||||
for (int i = -1; i < 2; i++) {
|
||||
for (int z = -1; z < 2; z++) {
|
||||
ChunkPos pos = ChunkPos(x+i, y+z, inst);
|
||||
|
||||
// if chunk exists, add it to the set
|
||||
if (chunkExists(pos))
|
||||
chnks.insert(chunks[pos]);
|
||||
}
|
||||
}
|
||||
|
||||
return chnks;
|
||||
}
|
||||
|
||||
/*
|
||||
* inefficient algorithm to get all chunks from a specific instance
|
||||
*/
|
||||
std::vector<ChunkPos> Chunking::getChunksInMap(uint64_t mapNum) {
|
||||
std::vector<ChunkPos> chnks;
|
||||
|
||||
for (auto it = chunks.begin(); it != chunks.end(); it++) {
|
||||
if (std::get<2>(it->first) == mapNum) {
|
||||
chnks.push_back(it->first);
|
||||
}
|
||||
}
|
||||
|
||||
return chnks;
|
||||
}
|
||||
|
||||
/*
|
||||
* Used only for eggs; use npc->playersInView for everything visible
|
||||
*/
|
||||
bool Chunking::inPopulatedChunks(std::set<Chunk*>* chnks) {
|
||||
for (auto it = chnks->begin(); it != chnks->end(); it++) {
|
||||
if ((*it)->nplayers > 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Chunking::createInstance(uint64_t instanceID) {
|
||||
std::vector<ChunkPos> templateChunks = getChunksInMap(MAPNUM(instanceID)); // base instance chunks
|
||||
|
||||
// only instantiate if the instance doesn't exist already
|
||||
if (getChunksInMap(instanceID).size() != 0) {
|
||||
std::cout << "Instance " << instanceID << " already exists" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "Creating instance " << instanceID << std::endl;
|
||||
for (ChunkPos &coords : templateChunks) {
|
||||
for (const EntityRef ref : chunks[coords]->entities) {
|
||||
if (ref.kind == EntityKind::PLAYER)
|
||||
continue;
|
||||
|
||||
int npcID = ref.id;
|
||||
BaseNPC* baseNPC = (BaseNPC*)ref.getEntity();
|
||||
|
||||
// make a copy of each NPC in the template chunks and put them in the new instance
|
||||
if (baseNPC->kind == EntityKind::MOB) {
|
||||
if (((Mob*)baseNPC)->groupLeader != 0 && ((Mob*)baseNPC)->groupLeader != npcID)
|
||||
continue; // follower; don't copy individually
|
||||
|
||||
Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->angle,
|
||||
instanceID, baseNPC->type, NPCManager::NPCData[baseNPC->type], NPCManager::nextId--);
|
||||
NPCManager::NPCs[newMob->id] = newMob;
|
||||
|
||||
// if in a group, copy over group members as well
|
||||
if (((Mob*)baseNPC)->groupLeader != 0) {
|
||||
newMob->groupLeader = newMob->id; // set leader ID for new leader
|
||||
Mob* mobData = (Mob*)baseNPC;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (mobData->groupMember[i] != 0) {
|
||||
int followerID = NPCManager::nextId--; // id for follower
|
||||
BaseNPC* baseFollower = NPCManager::NPCs[mobData->groupMember[i]]; // follower from template
|
||||
// new follower instance
|
||||
Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->angle,
|
||||
instanceID, baseFollower->type, NPCManager::NPCData[baseFollower->type], followerID);
|
||||
// add follower to NPC maps
|
||||
NPCManager::NPCs[followerID] = newMobFollower;
|
||||
// set follower-specific properties
|
||||
newMobFollower->groupLeader = newMob->id;
|
||||
newMobFollower->offsetX = ((Mob*)baseFollower)->offsetX;
|
||||
newMobFollower->offsetY = ((Mob*)baseFollower)->offsetY;
|
||||
// add follower copy to leader copy
|
||||
newMob->groupMember[i] = followerID;
|
||||
NPCManager::updateNPCPosition(followerID, baseFollower->x, baseFollower->y, baseFollower->z,
|
||||
instanceID, baseFollower->angle);
|
||||
}
|
||||
}
|
||||
}
|
||||
NPCManager::updateNPCPosition(newMob->id, baseNPC->x, baseNPC->y, baseNPC->z,
|
||||
instanceID, baseNPC->angle);
|
||||
} else {
|
||||
BaseNPC* newNPC = new BaseNPC(baseNPC->angle, instanceID, baseNPC->type, NPCManager::nextId--);
|
||||
NPCManager::NPCs[newNPC->id] = newNPC;
|
||||
NPCManager::updateNPCPosition(newNPC->id, baseNPC->x, baseNPC->y, baseNPC->z,
|
||||
instanceID, baseNPC->angle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void destroyInstance(uint64_t instanceID) {
|
||||
std::vector<ChunkPos> instanceChunks = getChunksInMap(instanceID);
|
||||
std::cout << "Deleting instance " << instanceID << " (" << instanceChunks.size() << " chunks)" << std::endl;
|
||||
for (ChunkPos& coords : instanceChunks) {
|
||||
emptyChunk(coords);
|
||||
}
|
||||
}
|
||||
|
||||
void Chunking::destroyInstanceIfEmpty(uint64_t instanceID) {
|
||||
if (PLAYERID(instanceID) == 0)
|
||||
return; // don't clean up overworld/IZ chunks
|
||||
|
||||
std::vector<ChunkPos> sourceChunkCoords = getChunksInMap(instanceID);
|
||||
|
||||
for (ChunkPos& coords : sourceChunkCoords) {
|
||||
Chunk* chunk = chunks[coords];
|
||||
|
||||
if (chunk->nplayers > 0)
|
||||
return; // there are still players inside
|
||||
}
|
||||
|
||||
destroyInstance(instanceID);
|
||||
}
|
||||
51
src/Chunking.hpp
Normal file
51
src/Chunking.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "EntityRef.hpp"
|
||||
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
class Chunk {
|
||||
public:
|
||||
std::set<EntityRef> entities;
|
||||
int nplayers = 0;
|
||||
};
|
||||
|
||||
// to help the readability of ChunkPos
|
||||
typedef std::tuple<int, int, uint64_t> _ChunkPos;
|
||||
|
||||
class ChunkPos : public _ChunkPos {
|
||||
public:
|
||||
ChunkPos() : _ChunkPos(0, 0, (uint64_t) -1) {}
|
||||
ChunkPos(int x, int y, uint64_t inst) : _ChunkPos(x, y, inst) {}
|
||||
};
|
||||
|
||||
enum {
|
||||
INSTANCE_OVERWORLD, // default instance every player starts in
|
||||
INSTANCE_IZ, // these aren't actually used
|
||||
INSTANCE_UNIQUE // these aren't actually used
|
||||
};
|
||||
|
||||
namespace Chunking {
|
||||
extern std::map<ChunkPos, Chunk*> chunks;
|
||||
|
||||
extern const ChunkPos INVALID_CHUNK;
|
||||
|
||||
void updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to);
|
||||
|
||||
void trackEntity(ChunkPos chunkPos, const EntityRef ref);
|
||||
void untrackEntity(ChunkPos chunkPos, const EntityRef ref);
|
||||
|
||||
void addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref);
|
||||
void removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref);
|
||||
|
||||
bool chunkExists(ChunkPos chunk);
|
||||
ChunkPos chunkPosAt(int posX, int posY, uint64_t instanceID);
|
||||
std::set<Chunk*> getViewableChunks(ChunkPos chunkPos);
|
||||
std::vector<ChunkPos> getChunksInMap(uint64_t mapNum);
|
||||
|
||||
bool inPopulatedChunks(std::set<Chunk*>* chnks);
|
||||
void createInstance(uint64_t);
|
||||
void destroyInstanceIfEmpty(uint64_t);
|
||||
}
|
||||
1011
src/Combat.cpp
Normal file
1011
src/Combat.cpp
Normal file
File diff suppressed because it is too large
Load Diff
23
src/Combat.hpp
Normal file
23
src/Combat.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "MobAI.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
struct Bullet {
|
||||
int pointDamage;
|
||||
int groupDamage;
|
||||
bool weaponBoost;
|
||||
int bulletType;
|
||||
};
|
||||
|
||||
namespace Combat {
|
||||
extern std::map<int32_t, std::map<int8_t, Bullet>> Bullets;
|
||||
|
||||
void init();
|
||||
|
||||
void npcAttackPc(Mob *mob, time_t currTime);
|
||||
void genQItemRolls(std::vector<Player*> players, std::map<int, int>& rolls);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
1306
src/CustomCommands.cpp
Normal file
1306
src/CustomCommands.cpp
Normal file
File diff suppressed because it is too large
Load Diff
11
src/CustomCommands.hpp
Normal file
11
src/CustomCommands.hpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace CustomCommands {
|
||||
void init();
|
||||
|
||||
bool runCmd(std::string full, CNSocket* sock);
|
||||
};
|
||||
357
src/Database.cpp
357
src/Database.cpp
@@ -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
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
220
src/Defines.cpp
220
src/Defines.cpp
@@ -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";
|
||||
}
|
||||
246
src/Eggs.cpp
Normal file
246
src/Eggs.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
#include "Eggs.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Abilities.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
#include "Entities.hpp"
|
||||
#include "Items.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
using namespace Eggs;
|
||||
|
||||
std::unordered_map<int, EggType> Eggs::EggTypes;
|
||||
|
||||
void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// eggId might be 0 if the buff is made by the /buff command
|
||||
EntityRef src = eggId == 0 ? sock : EntityRef(eggId);
|
||||
|
||||
if(Abilities::SkillTable.count(skillId) == 0) {
|
||||
std::cout << "[WARN] egg " << eggId << " has skill ID " << skillId << " which doesn't exist" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
SkillResult result = SkillResult();
|
||||
SkillData* skill = &Abilities::SkillTable[skillId];
|
||||
if(skill->drainType == SkillDrainType::PASSIVE) {
|
||||
// apply buff
|
||||
if(skill->targetType != SkillTargetType::PLAYERS) {
|
||||
std::cout << "[WARN] weird skill type for egg " << eggId << " with skill " << skillId << ", should be " << (int)skill->targetType << std::endl;
|
||||
}
|
||||
|
||||
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
|
||||
int value = skill->values[0][0];
|
||||
BuffStack eggBuff = {
|
||||
duration * 1000 / MS_PER_PLAYER_TICK,
|
||||
value,
|
||||
src,
|
||||
BuffClass::EGG
|
||||
};
|
||||
plr->addBuff(timeBuffId,
|
||||
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
|
||||
Buffs::timeBuffUpdate(self, buff, status, stack);
|
||||
if(status == ETBU_DEL) Buffs::timeBuffTimeout(self);
|
||||
},
|
||||
[](EntityRef self, Buff* buff, time_t currTime) {
|
||||
// no-op
|
||||
},
|
||||
&eggBuff);
|
||||
|
||||
sSkillResult_Buff resultBuff{};
|
||||
resultBuff.eCT = plr->getCharType();
|
||||
resultBuff.iID = plr->getID();
|
||||
resultBuff.bProtected = false;
|
||||
resultBuff.iConditionBitFlag = plr->getCompositeCondition();
|
||||
result = SkillResult(sizeof(sSkillResult_Buff), &resultBuff);
|
||||
} else {
|
||||
int value = plr->getMaxHP() * skill->values[0][0] / 1000;
|
||||
sSkillResult_Damage resultDamage{};
|
||||
sSkillResult_Heal_HP resultHeal{};
|
||||
switch(skill->skillType)
|
||||
{
|
||||
case SkillType::DAMAGE:
|
||||
resultDamage.bProtected = false;
|
||||
resultDamage.eCT = plr->getCharType();
|
||||
resultDamage.iID = plr->getID();
|
||||
resultDamage.iDamage = plr->takeDamage(src, value);
|
||||
resultDamage.iHP = plr->getCurrentHP();
|
||||
result = SkillResult(sizeof(sSkillResult_Damage), &resultDamage);
|
||||
break;
|
||||
case SkillType::HEAL_HP:
|
||||
resultHeal.eCT = plr->getCharType();
|
||||
resultHeal.iID = plr->getID();
|
||||
resultHeal.iHealHP = plr->heal(src, value);
|
||||
resultHeal.iHP = plr->getCurrentHP();
|
||||
result = SkillResult(sizeof(sSkillResult_Heal_HP), &resultHeal);
|
||||
break;
|
||||
default:
|
||||
std::cout << "[WARN] oops, egg with active skill type " << (int)skill->skillType << " unhandled";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + result.size;
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
|
||||
pkt->iNPC_ID = eggId;
|
||||
pkt->iSkillID = skillId;
|
||||
pkt->eST = (int32_t)skill->skillType;
|
||||
pkt->iTargetCnt = 1;
|
||||
|
||||
if(result.size > 0) {
|
||||
void* attached = (void*)(pkt + 1);
|
||||
memcpy(attached, result.payload, result.size);
|
||||
}
|
||||
|
||||
NPCManager::sendToViewable(src.getEntity(), pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
|
||||
}
|
||||
|
||||
static void eggStep(CNServer* serv, time_t currTime) {
|
||||
// check dead eggs and eggs in inactive chunks
|
||||
for (auto npc : NPCManager::NPCs) {
|
||||
if (npc.second->kind != EntityKind::EGG)
|
||||
continue;
|
||||
|
||||
auto egg = (Egg*)npc.second;
|
||||
if (!egg->dead || !Chunking::inPopulatedChunks(&egg->viewableChunks))
|
||||
continue;
|
||||
|
||||
if (egg->deadUntil <= currTime) {
|
||||
// respawn it
|
||||
egg->dead = false;
|
||||
egg->deadUntil = 0;
|
||||
egg->hp = 400;
|
||||
|
||||
Chunking::addEntityToChunks(Chunking::getViewableChunks(egg->chunkPos), {npc.first});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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->kind != EntityKind::EGG) {
|
||||
std::cout << "[WARN] Player tried to open something other than an?!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (egg->dead) {
|
||||
std::cout << "[WARN] Player tried to open a dead egg?!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
/* this has some issues with position desync, leaving it out for now
|
||||
if (abs(egg->x - plr->x)>500 || abs(egg->y - plr->y) > 500) {
|
||||
std::cout << "[WARN] Player tried to open an egg isn't nearby?!" << std::endl;
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
int typeId = egg->type;
|
||||
if (EggTypes.find(typeId) == EggTypes.end()) {
|
||||
std::cout << "[WARN] Egg Type " << typeId << " not found!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
EggType* type = &EggTypes[typeId];
|
||||
|
||||
/*
|
||||
* SHINY_PICKUP_SUCC is only causing a GUI effect in the client
|
||||
* (buff icon pops up in the bottom of the screen)
|
||||
* so we don't send it for non-effect
|
||||
*/
|
||||
if (type->effectId != 0) {
|
||||
eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration);
|
||||
INITSTRUCT(sP_FE2CL_REP_SHINY_PICKUP_SUCC, resp);
|
||||
resp.iSkillID = type->effectId;
|
||||
|
||||
// in general client finds correct icon on it's own,
|
||||
// but for damage we have to supply correct CSTB
|
||||
if (resp.iSkillID == 183)
|
||||
resp.eCSTB = ECSB_INFECTION;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_SHINY_PICKUP_SUCC);
|
||||
}
|
||||
|
||||
// drop
|
||||
if (type->dropCrateId != 0) {
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_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->hp = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Eggs::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SHINY_PICKUP, eggPickup);
|
||||
|
||||
REGISTER_SHARD_TIMER(eggStep, 1000);
|
||||
}
|
||||
19
src/Eggs.hpp
Normal file
19
src/Eggs.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
struct EggType {
|
||||
int dropCrateId;
|
||||
int effectId;
|
||||
int duration;
|
||||
int regen;
|
||||
};
|
||||
|
||||
namespace Eggs {
|
||||
extern std::unordered_map<int, EggType> EggTypes;
|
||||
|
||||
void init();
|
||||
|
||||
void eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration);
|
||||
void npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg);
|
||||
}
|
||||
346
src/Email.cpp
Normal file
346
src/Email.cpp
Normal file
@@ -0,0 +1,346 @@
|
||||
#include "Email.hpp"
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "db/Database.hpp"
|
||||
#include "servers/CNShardServer.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);
|
||||
|
||||
// notification to recipient if online
|
||||
CNSocket* recipient = PlayerManager::getSockFromID(pkt->iTo_PCUID);
|
||||
if (recipient != nullptr)
|
||||
{
|
||||
emailUpdateCheck(recipient, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void Email::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_EMAIL_UPDATE_CHECK, emailUpdateCheck);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST, emailReceivePageList);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_READ_EMAIL, emailRead);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_CANDY, emailReceiveTaros);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM, emailReceiveItemSingle);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL, emailReceiveItemAll);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_DELETE_EMAIL, emailDelete);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SEND_EMAIL, emailSend);
|
||||
}
|
||||
10
src/Email.hpp
Normal file
10
src/Email.hpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace Email {
|
||||
extern std::vector<std::string> dump;
|
||||
|
||||
void init();
|
||||
}
|
||||
147
src/Entities.cpp
Normal file
147
src/Entities.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
#include "Entities.hpp"
|
||||
|
||||
#include "NPCManager.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
static_assert(std::is_standard_layout<EntityRef>::value);
|
||||
static_assert(std::is_trivially_copyable<EntityRef>::value);
|
||||
|
||||
EntityRef::EntityRef(CNSocket *s) {
|
||||
kind = EntityKind::PLAYER;
|
||||
sock = s;
|
||||
}
|
||||
|
||||
EntityRef::EntityRef(int32_t i) {
|
||||
id = i;
|
||||
|
||||
kind = EntityKind::INVALID;
|
||||
if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end())
|
||||
kind = NPCManager::NPCs[id]->kind;
|
||||
}
|
||||
|
||||
bool EntityRef::isValid() const {
|
||||
if (kind == EntityKind::PLAYER)
|
||||
return PlayerManager::players.find(sock) != PlayerManager::players.end();
|
||||
|
||||
return NPCManager::NPCs.find(id) != NPCManager::NPCs.end();
|
||||
}
|
||||
|
||||
Entity *EntityRef::getEntity() const {
|
||||
assert(isValid());
|
||||
|
||||
if (kind == EntityKind::PLAYER)
|
||||
return PlayerManager::getPlayer(sock);
|
||||
|
||||
return NPCManager::NPCs[id];
|
||||
}
|
||||
|
||||
sNPCAppearanceData BaseNPC::getAppearanceData() {
|
||||
sNPCAppearanceData data = {};
|
||||
data.iAngle = angle;
|
||||
data.iBarkerType = 0; // unused?
|
||||
data.iConditionBitFlag = 0;
|
||||
data.iHP = hp;
|
||||
data.iNPCType = type;
|
||||
data.iNPC_ID = id;
|
||||
data.iX = x;
|
||||
data.iY = y;
|
||||
data.iZ = z;
|
||||
return data;
|
||||
}
|
||||
|
||||
sNPCAppearanceData CombatNPC::getAppearanceData() {
|
||||
sNPCAppearanceData data = BaseNPC::getAppearanceData();
|
||||
data.iConditionBitFlag = getCompositeCondition();
|
||||
return data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Entity coming into view.
|
||||
*/
|
||||
void BaseNPC::enterIntoViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
|
||||
pkt.NPCAppearanceData = getAppearanceData();
|
||||
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
|
||||
}
|
||||
|
||||
void Bus::enterIntoViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_TRANSPORTATION_ENTER, pkt);
|
||||
|
||||
// TODO: Potentially decouple this from BaseNPC?
|
||||
pkt.AppearanceData = {
|
||||
3, id, type,
|
||||
x, y, z
|
||||
};
|
||||
|
||||
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_ENTER);
|
||||
}
|
||||
|
||||
void Egg::enterIntoViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt);
|
||||
|
||||
// TODO: Potentially decouple this from BaseNPC?
|
||||
pkt.ShinyAppearanceData = {
|
||||
id, type, 0, // client doesn't care about map num
|
||||
x, y, z
|
||||
};
|
||||
|
||||
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER);
|
||||
}
|
||||
|
||||
sNano* Player::getActiveNano() {
|
||||
return &Nanos[activeNano];
|
||||
}
|
||||
|
||||
sPCAppearanceData Player::getAppearanceData() {
|
||||
sPCAppearanceData data = {};
|
||||
data.iID = iID;
|
||||
data.iHP = HP;
|
||||
data.iLv = level;
|
||||
data.iX = x;
|
||||
data.iY = y;
|
||||
data.iZ = z;
|
||||
data.iAngle = angle;
|
||||
data.PCStyle = PCStyle;
|
||||
data.Nano = Nanos[activeNano];
|
||||
data.iPCState = iPCState;
|
||||
data.iSpecialState = iSpecialState;
|
||||
memcpy(data.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT);
|
||||
return data;
|
||||
}
|
||||
|
||||
// TODO: this is less effiecient than it was, because of memset()
|
||||
void Player::enterIntoViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_PC_NEW, pkt);
|
||||
pkt.PCAppearanceData = getAppearanceData();
|
||||
sock->sendPacket(pkt, P_FE2CL_PC_NEW);
|
||||
}
|
||||
|
||||
/*
|
||||
* Entity leaving view.
|
||||
*/
|
||||
void BaseNPC::disappearFromViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
|
||||
pkt.iNPC_ID = id;
|
||||
sock->sendPacket(pkt, P_FE2CL_NPC_EXIT);
|
||||
}
|
||||
|
||||
void Bus::disappearFromViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_TRANSPORTATION_EXIT, pkt);
|
||||
pkt.eTT = 3;
|
||||
pkt.iT_ID = id;
|
||||
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_EXIT);
|
||||
}
|
||||
|
||||
void Egg::disappearFromViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_SHINY_EXIT, pkt);
|
||||
pkt.iShinyID = id;
|
||||
sock->sendPacket(pkt, P_FE2CL_SHINY_EXIT);
|
||||
}
|
||||
|
||||
void Player::disappearFromViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_PC_EXIT, pkt);
|
||||
pkt.iID = iID;
|
||||
sock->sendPacket(pkt, P_FE2CL_PC_EXIT);
|
||||
}
|
||||
175
src/Entities.hpp
Normal file
175
src/Entities.hpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include "EntityRef.hpp"
|
||||
#include "Buffs.hpp"
|
||||
#include "Chunking.hpp"
|
||||
#include "Groups.hpp"
|
||||
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
|
||||
enum class AIState {
|
||||
INACTIVE,
|
||||
ROAMING,
|
||||
COMBAT,
|
||||
RETREAT,
|
||||
DEAD
|
||||
};
|
||||
|
||||
struct Entity {
|
||||
EntityKind kind = EntityKind::INVALID;
|
||||
int x = 0, y = 0, z = 0;
|
||||
uint64_t instanceID = 0;
|
||||
ChunkPos chunkPos = {};
|
||||
std::set<Chunk*> viewableChunks = {};
|
||||
|
||||
// destructor must be virtual, apparently
|
||||
virtual ~Entity() {}
|
||||
|
||||
virtual bool isExtant() { return true; }
|
||||
|
||||
// stubs
|
||||
virtual void enterIntoViewOf(CNSocket *sock) = 0;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) = 0;
|
||||
};
|
||||
|
||||
/*
|
||||
* Interfaces
|
||||
*/
|
||||
class ICombatant {
|
||||
public:
|
||||
ICombatant() {}
|
||||
virtual ~ICombatant() {}
|
||||
|
||||
virtual bool addBuff(int, BuffCallback<int, BuffStack*>, BuffCallback<time_t>, BuffStack*) = 0;
|
||||
virtual Buff* getBuff(int) = 0;
|
||||
virtual void removeBuff(int) = 0;
|
||||
virtual void removeBuff(int, BuffClass) = 0;
|
||||
virtual void clearBuffs(bool) = 0;
|
||||
virtual bool hasBuff(int) = 0;
|
||||
virtual int getCompositeCondition() = 0;
|
||||
virtual int takeDamage(EntityRef, int) = 0;
|
||||
virtual int heal(EntityRef, int) = 0;
|
||||
virtual bool isAlive() = 0;
|
||||
virtual int getCurrentHP() = 0;
|
||||
virtual int getMaxHP() = 0;
|
||||
virtual int getLevel() = 0;
|
||||
virtual std::vector<EntityRef> getGroupMembers() = 0;
|
||||
virtual int32_t getCharType() = 0;
|
||||
virtual int32_t getID() = 0;
|
||||
virtual EntityRef getRef() = 0;
|
||||
virtual void step(time_t currTime) = 0;
|
||||
};
|
||||
|
||||
/*
|
||||
* Subclasses
|
||||
*/
|
||||
class BaseNPC : public Entity {
|
||||
public:
|
||||
int id;
|
||||
int type;
|
||||
int hp;
|
||||
int angle;
|
||||
bool loopingPath = false;
|
||||
|
||||
BaseNPC(int _A, uint64_t iID, int t, int _id) {
|
||||
kind = EntityKind::SIMPLE_NPC;
|
||||
type = t;
|
||||
hp = 400;
|
||||
angle = _A;
|
||||
id = _id;
|
||||
instanceID = iID;
|
||||
};
|
||||
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
|
||||
virtual sNPCAppearanceData getAppearanceData();
|
||||
};
|
||||
|
||||
struct CombatNPC : public BaseNPC, public ICombatant {
|
||||
int maxHealth = 0;
|
||||
int spawnX = 0;
|
||||
int spawnY = 0;
|
||||
int spawnZ = 0;
|
||||
int level = 0;
|
||||
int speed = 300;
|
||||
AIState state = AIState::INACTIVE;
|
||||
Group* group = nullptr;
|
||||
int playersInView = 0; // for optimizing away AI in empty chunks
|
||||
|
||||
std::map<AIState, void (*)(CombatNPC*, time_t)> stateHandlers;
|
||||
std::map<AIState, void (*)(CombatNPC*, EntityRef)> transitionHandlers;
|
||||
|
||||
std::unordered_map<int, Buff*> buffs = {};
|
||||
|
||||
CombatNPC(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, int id, int maxHP)
|
||||
: BaseNPC(angle, iID, t, id), maxHealth(maxHP) {
|
||||
this->spawnX = spawnX;
|
||||
this->spawnY = spawnY;
|
||||
this->spawnZ = spawnZ;
|
||||
|
||||
kind = EntityKind::COMBAT_NPC;
|
||||
|
||||
stateHandlers[AIState::INACTIVE] = {};
|
||||
transitionHandlers[AIState::INACTIVE] = {};
|
||||
}
|
||||
|
||||
virtual sNPCAppearanceData getAppearanceData() override;
|
||||
|
||||
virtual bool isExtant() override { return hp > 0; }
|
||||
|
||||
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
|
||||
virtual Buff* getBuff(int buffId) override;
|
||||
virtual void removeBuff(int buffId) override;
|
||||
virtual void removeBuff(int buffId, BuffClass buffClass) override;
|
||||
virtual void clearBuffs(bool force) override;
|
||||
virtual bool hasBuff(int buffId) override;
|
||||
virtual int getCompositeCondition() override;
|
||||
virtual int takeDamage(EntityRef src, int amt) override;
|
||||
virtual int heal(EntityRef src, int amt) override;
|
||||
virtual bool isAlive() override;
|
||||
virtual int getCurrentHP() override;
|
||||
virtual int getMaxHP() override;
|
||||
virtual int getLevel() override;
|
||||
virtual std::vector<EntityRef> getGroupMembers() override;
|
||||
virtual int32_t getCharType() override;
|
||||
virtual int32_t getID() override;
|
||||
virtual EntityRef getRef() override;
|
||||
virtual void step(time_t currTime) override;
|
||||
|
||||
virtual void transition(AIState newState, EntityRef src);
|
||||
};
|
||||
|
||||
// Mob is in MobAI.hpp, Player is in Player.hpp
|
||||
|
||||
struct Egg : public BaseNPC {
|
||||
bool summoned = false;
|
||||
bool dead = false;
|
||||
time_t deadUntil;
|
||||
|
||||
Egg(uint64_t iID, int t, int32_t id, bool summon)
|
||||
: BaseNPC(0, iID, t, id) {
|
||||
summoned = summon;
|
||||
kind = EntityKind::EGG;
|
||||
}
|
||||
|
||||
virtual bool isExtant() override { return !dead; }
|
||||
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
};
|
||||
|
||||
struct Bus : public BaseNPC {
|
||||
Bus(int angle, uint64_t iID, int t, int id) :
|
||||
BaseNPC(angle, iID, t, id) {
|
||||
kind = EntityKind::BUS;
|
||||
loopingPath = true;
|
||||
}
|
||||
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
};
|
||||
56
src/EntityRef.hpp
Normal file
56
src/EntityRef.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
/* forward declaration(s) */
|
||||
struct Entity;
|
||||
|
||||
enum EntityKind {
|
||||
INVALID,
|
||||
PLAYER,
|
||||
SIMPLE_NPC,
|
||||
COMBAT_NPC,
|
||||
MOB,
|
||||
EGG,
|
||||
BUS
|
||||
};
|
||||
|
||||
struct EntityRef {
|
||||
EntityKind kind;
|
||||
union {
|
||||
CNSocket *sock;
|
||||
int32_t id;
|
||||
};
|
||||
|
||||
EntityRef(CNSocket *s);
|
||||
EntityRef(int32_t i);
|
||||
|
||||
bool isValid() const;
|
||||
Entity *getEntity() const;
|
||||
|
||||
bool operator==(const EntityRef& other) const {
|
||||
if (kind != other.kind)
|
||||
return false;
|
||||
|
||||
if (kind == EntityKind::PLAYER)
|
||||
return sock == other.sock;
|
||||
|
||||
return id == other.id;
|
||||
}
|
||||
|
||||
bool operator!=(const EntityRef& other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
// arbitrary ordering
|
||||
bool operator<(const EntityRef& other) const {
|
||||
if (kind == other.kind) {
|
||||
if (kind == EntityKind::PLAYER)
|
||||
return sock < other.sock;
|
||||
else
|
||||
return id < other.id;
|
||||
}
|
||||
|
||||
return kind < other.kind;
|
||||
}
|
||||
};
|
||||
329
src/Groups.cpp
Normal file
329
src/Groups.cpp
Normal file
@@ -0,0 +1,329 @@
|
||||
#include "Groups.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Entities.hpp"
|
||||
|
||||
/*
|
||||
* NOTE: Variadic response packets that list group members are technically
|
||||
* double-variadic, as they have two count members with trailing struct counts,
|
||||
* and are thus incompatible with the generic sendPacket() wrapper.
|
||||
* That means we still have to (carefully) use validOutVarPacket() in this
|
||||
* source file.
|
||||
*/
|
||||
|
||||
using namespace Groups;
|
||||
|
||||
Group::Group(EntityRef leader) {
|
||||
addToGroup(this, leader);
|
||||
}
|
||||
|
||||
static void attachGroupData(std::vector<EntityRef>& pcs, std::vector<EntityRef>& npcs, uint8_t* pivot) {
|
||||
for(EntityRef pcRef : pcs) {
|
||||
sPCGroupMemberInfo* info = (sPCGroupMemberInfo*)pivot;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(pcRef.sock);
|
||||
info->iPC_ID = plr->iID;
|
||||
info->iPCUID = plr->PCStyle.iPC_UID;
|
||||
info->iNameCheck = plr->PCStyle.iNameCheck;
|
||||
memcpy(info->szFirstName, plr->PCStyle.szFirstName, sizeof(plr->PCStyle.szFirstName));
|
||||
memcpy(info->szLastName, plr->PCStyle.szLastName, sizeof(plr->PCStyle.szLastName));
|
||||
info->iSpecialState = plr->iSpecialState;
|
||||
info->iLv = plr->level;
|
||||
info->iHP = plr->HP;
|
||||
info->iMaxHP = PC_MAXHEALTH(plr->level);
|
||||
// info->iMapType = 0;
|
||||
// info->iMapNum = 0;
|
||||
info->iX = plr->x;
|
||||
info->iY = plr->y;
|
||||
info->iZ = plr->z;
|
||||
if(plr->activeNano > 0) {
|
||||
info->Nano = *plr->getActiveNano();
|
||||
info->bNano = true;
|
||||
}
|
||||
|
||||
pivot = (uint8_t*)(info + 1);
|
||||
}
|
||||
for(EntityRef npcRef : npcs) {
|
||||
sNPCGroupMemberInfo* info = (sNPCGroupMemberInfo*)pivot;
|
||||
|
||||
// probably should not assume that the combatant is an
|
||||
// entity, but it works for now
|
||||
BaseNPC* npc = (BaseNPC*)npcRef.getEntity();
|
||||
info->iNPC_ID = npcRef.id;
|
||||
info->iNPC_Type = npc->type;
|
||||
info->iHP = npc->hp;
|
||||
info->iX = npc->x;
|
||||
info->iY = npc->y;
|
||||
info->iZ = npc->z;
|
||||
|
||||
pivot = (uint8_t*)(info + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void Groups::addToGroup(Group* group, EntityRef member) {
|
||||
if (group == nullptr)
|
||||
return;
|
||||
|
||||
if (member.kind == EntityKind::PLAYER) {
|
||||
Player* plr = PlayerManager::getPlayer(member.sock);
|
||||
plr->group = group;
|
||||
}
|
||||
else if (member.kind == EntityKind::COMBAT_NPC) {
|
||||
CombatNPC* npc = (CombatNPC*)member.getEntity();
|
||||
npc->group = group;
|
||||
}
|
||||
else {
|
||||
std::cout << "[WARN] Adding a weird entity type to a group" << std::endl;
|
||||
}
|
||||
|
||||
group->members.push_back(member);
|
||||
|
||||
if(member.kind == EntityKind::PLAYER) {
|
||||
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
|
||||
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
|
||||
size_t pcCount = pcs.size();
|
||||
size_t npcCount = npcs.size();
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
|
||||
sP_FE2CL_PC_GROUP_JOIN* pkt = (sP_FE2CL_PC_GROUP_JOIN*)respbuf;
|
||||
|
||||
pkt->iID_NewMember = PlayerManager::getPlayer(member.sock)->iID;
|
||||
pkt->iMemberPCCnt = (int32_t)pcCount;
|
||||
pkt->iMemberNPCCnt = (int32_t)npcCount;
|
||||
|
||||
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), pcCount, sizeof(sPCGroupMemberInfo))
|
||||
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size" << std::endl;
|
||||
} else {
|
||||
uint8_t* pivot = (uint8_t*)(pkt + 1);
|
||||
attachGroupData(pcs, npcs, pivot);
|
||||
// PC_GROUP_JOIN_SUCC and PC_GROUP_JOIN carry identical payloads but have different IDs
|
||||
// (and the client does care!) so we need to send one to the new member
|
||||
// and the other to the rest
|
||||
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo);
|
||||
member.sock->sendPacket(respbuf, P_FE2CL_PC_GROUP_JOIN_SUCC, resplen);
|
||||
sendToGroup(group, member, respbuf, P_FE2CL_PC_GROUP_JOIN, resplen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Groups::removeFromGroup(Group* group, EntityRef member) {
|
||||
if (group == nullptr)
|
||||
return false;
|
||||
|
||||
if (member.kind == EntityKind::PLAYER) {
|
||||
Player* plr = PlayerManager::getPlayer(member.sock);
|
||||
plr->group = nullptr; // no dangling pointers here muahaahahah
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, leavePkt);
|
||||
member.sock->sendPacket(leavePkt, P_FE2CL_PC_GROUP_LEAVE_SUCC);
|
||||
}
|
||||
else if (member.kind == EntityKind::COMBAT_NPC) {
|
||||
CombatNPC* npc = (CombatNPC*)member.getEntity();
|
||||
npc->group = nullptr;
|
||||
}
|
||||
else {
|
||||
std::cout << "[WARN] Removing a weird entity type from a group" << std::endl;
|
||||
}
|
||||
|
||||
auto it = std::find(group->members.begin(), group->members.end(), member);
|
||||
if (it == group->members.end()) {
|
||||
std::cout << "[WARN] Tried to remove a member that isn't in the group" << std::endl;
|
||||
} else {
|
||||
group->members.erase(it);
|
||||
}
|
||||
|
||||
if(member.kind == EntityKind::PLAYER) {
|
||||
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
|
||||
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
|
||||
size_t pcCount = pcs.size();
|
||||
size_t npcCount = npcs.size();
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
|
||||
sP_FE2CL_PC_GROUP_LEAVE* pkt = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf;
|
||||
|
||||
pkt->iID_LeaveMember = PlayerManager::getPlayer(member.sock)->iID;
|
||||
pkt->iMemberPCCnt = (int32_t)pcCount;
|
||||
pkt->iMemberNPCCnt = (int32_t)npcCount;
|
||||
|
||||
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), pcCount, sizeof(sPCGroupMemberInfo))
|
||||
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size" << std::endl;
|
||||
} else {
|
||||
uint8_t* pivot = (uint8_t*)(pkt + 1);
|
||||
attachGroupData(pcs, npcs, pivot);
|
||||
sendToGroup(group, respbuf, P_FE2CL_PC_GROUP_LEAVE,
|
||||
sizeof(sP_FE2CL_PC_GROUP_LEAVE) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo));
|
||||
}
|
||||
}
|
||||
|
||||
if (group->members.size() == 1) {
|
||||
return removeFromGroup(group, group->members.back());
|
||||
}
|
||||
|
||||
if (group->members.empty()) {
|
||||
delete group; // cleanup memory
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Groups::disbandGroup(Group* group) {
|
||||
if (group == nullptr)
|
||||
return;
|
||||
|
||||
// remove everyone from the group!!
|
||||
bool done = false;
|
||||
while(!done) {
|
||||
EntityRef back = group->members.back();
|
||||
done = removeFromGroup(group, back);
|
||||
}
|
||||
}
|
||||
|
||||
static void requestGroup(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_GROUP_INVITE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_To);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
// fail if the group is full or the other player is already in a group
|
||||
if ((plr->group != nullptr && plr->group->filter(EntityKind::PLAYER).size() >= 4) || otherPlr->group != nullptr) {
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_FAIL, resp);
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_FAIL, sizeof(sP_FE2CL_PC_GROUP_INVITE_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(recv->iID_To);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE, resp);
|
||||
|
||||
resp.iHostID = plr->iID;
|
||||
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE, sizeof(sP_FE2CL_PC_GROUP_INVITE));
|
||||
}
|
||||
|
||||
static void refuseGroup(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_GROUP_INVITE_REFUSE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE_REFUSE*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(recv->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_REFUSE, resp);
|
||||
|
||||
resp.iID_To = plr->iID;
|
||||
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_REFUSE, sizeof(sP_FE2CL_PC_GROUP_INVITE_REFUSE));
|
||||
}
|
||||
|
||||
static void joinGroup(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_GROUP_JOIN* recv = (sP_CL2FE_REQ_PC_GROUP_JOIN*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_From);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return; // disconnect or something
|
||||
|
||||
int size = otherPlr->group == nullptr ? 1 : otherPlr->group->filter(EntityKind::PLAYER).size();
|
||||
|
||||
// fail if the group is full or the other player is already in a group
|
||||
if (plr->group != nullptr || size + 1 > 4) {
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_JOIN_FAIL, resp);
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_JOIN_FAIL, sizeof(sP_FE2CL_PC_GROUP_JOIN_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
if (otherPlr->group == nullptr) {
|
||||
// create group
|
||||
EntityRef otherPlrRef = PlayerManager::getSockFromID(recv->iID_From);
|
||||
otherPlr->group = new Group(otherPlrRef);
|
||||
}
|
||||
addToGroup(otherPlr->group, sock);
|
||||
}
|
||||
|
||||
static void leaveGroup(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
groupKick(plr->group, sock);
|
||||
}
|
||||
|
||||
void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) {
|
||||
if (group == nullptr)
|
||||
return;
|
||||
|
||||
auto players = group->filter(EntityKind::PLAYER);
|
||||
for (EntityRef ref : players) {
|
||||
ref.sock->sendPacket(buf, type, size);
|
||||
}
|
||||
}
|
||||
|
||||
void Groups::sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size) {
|
||||
if (group == nullptr)
|
||||
return;
|
||||
|
||||
auto players = group->filter(EntityKind::PLAYER);
|
||||
for (EntityRef ref : players) {
|
||||
if(ref != excluded) ref.sock->sendPacket(buf, type, size);
|
||||
}
|
||||
}
|
||||
|
||||
void Groups::groupTickInfo(CNSocket* sock) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Group* group = plr->group;
|
||||
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
|
||||
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
|
||||
size_t pcCount = pcs.size();
|
||||
size_t npcCount = npcs.size();
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
|
||||
sP_FE2CL_PC_GROUP_MEMBER_INFO* pkt = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf;
|
||||
|
||||
pkt->iID = plr->iID;
|
||||
pkt->iMemberPCCnt = (int32_t)pcCount;
|
||||
pkt->iMemberNPCCnt = (int32_t)npcCount;
|
||||
|
||||
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), pcCount, sizeof(sPCGroupMemberInfo))
|
||||
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_MEMBER_INFO packet size" << std::endl;
|
||||
} else {
|
||||
uint8_t* pivot = (uint8_t*)(pkt + 1);
|
||||
attachGroupData(pcs, npcs, pivot);
|
||||
sock->sendPacket(respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO,
|
||||
sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo));
|
||||
}
|
||||
}
|
||||
|
||||
void Groups::groupKick(Group* group, EntityRef ref) {
|
||||
|
||||
if (group == nullptr)
|
||||
return;
|
||||
|
||||
// if you are the group leader, destroy your own group and kick everybody
|
||||
if (group->members[0] == ref) {
|
||||
disbandGroup(group);
|
||||
return;
|
||||
}
|
||||
|
||||
removeFromGroup(group, ref);
|
||||
}
|
||||
|
||||
void Groups::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_INVITE, requestGroup);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_INVITE_REFUSE, refuseGroup);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_JOIN, joinGroup);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_LEAVE, leaveGroup);
|
||||
}
|
||||
37
src/Groups.hpp
Normal file
37
src/Groups.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "EntityRef.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <assert.h>
|
||||
|
||||
struct Group {
|
||||
std::vector<EntityRef> members;
|
||||
|
||||
std::vector<EntityRef> filter(EntityKind kind) {
|
||||
std::vector<EntityRef> filtered;
|
||||
std::copy_if(members.begin(), members.end(), std::back_inserter(filtered), [kind](EntityRef e) {
|
||||
return e.kind == kind;
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
EntityRef getLeader() {
|
||||
assert(members.size() > 0);
|
||||
return members[0];
|
||||
}
|
||||
|
||||
Group(EntityRef leader);
|
||||
};
|
||||
|
||||
namespace Groups {
|
||||
void init();
|
||||
|
||||
void sendToGroup(Group* group, void* buf, uint32_t type, size_t size);
|
||||
void sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size);
|
||||
void groupTickInfo(CNSocket* sock);
|
||||
|
||||
void groupKick(Group* group, EntityRef ref);
|
||||
void addToGroup(Group* group, EntityRef member);
|
||||
bool removeFromGroup(Group* group, EntityRef member); // true iff group deleted
|
||||
void disbandGroup(Group* group);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
865
src/Items.cpp
Normal file
865
src/Items.cpp
Normal file
@@ -0,0 +1,865 @@
|
||||
#include "Items.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "Abilities.hpp"
|
||||
#include "Eggs.hpp"
|
||||
#include "MobAI.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Buffs.hpp"
|
||||
|
||||
#include <string.h> // for memset()
|
||||
#include <assert.h>
|
||||
#include <numeric>
|
||||
|
||||
using namespace Items;
|
||||
|
||||
std::map<std::pair<int32_t, int32_t>, Items::Item> Items::ItemData;
|
||||
std::map<int32_t, CrocPotEntry> Items::CrocPotTable;
|
||||
std::map<int32_t, std::vector<int32_t>> Items::RarityWeights;
|
||||
std::map<int32_t, Crate> Items::Crates;
|
||||
std::map<int32_t, ItemReference> Items::ItemReferences;
|
||||
std::map<std::string, std::vector<std::pair<int32_t, int32_t>>> Items::CodeItems;
|
||||
|
||||
std::map<int32_t, CrateDropChance> Items::CrateDropChances;
|
||||
std::map<int32_t, std::vector<int32_t>> Items::CrateDropTypes;
|
||||
std::map<int32_t, MiscDropChance> Items::MiscDropChances;
|
||||
std::map<int32_t, MiscDropType> Items::MiscDropTypes;
|
||||
std::map<int32_t, MobDrop> Items::MobDrops;
|
||||
std::map<int32_t, int32_t> Items::EventToDropMap;
|
||||
std::map<int32_t, int32_t> Items::MobToDropMap;
|
||||
std::map<int32_t, ItemSet> Items::ItemSets;
|
||||
|
||||
#ifdef ACADEMY
|
||||
std::map<int32_t, int32_t> Items::NanoCapsules; // crate id -> nano id
|
||||
|
||||
static void nanoCapsuleHandler(CNSocket* sock, int slot, sItemBase *chest) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
int32_t nanoId = NanoCapsules[chest->iID];
|
||||
|
||||
// chest opening acknowledgement packet
|
||||
INITSTRUCT(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, resp);
|
||||
resp.iSlotNum = slot;
|
||||
|
||||
// in order to remove capsule form inventory, we have to send item reward packet with empty item
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_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);
|
||||
|
||||
if (itemdel->iSlotNum < 0 || itemdel->iSlotNum >= AINVEN_COUNT)
|
||||
return; // sanity check
|
||||
|
||||
resp.eIL = itemdel->eIL;
|
||||
resp.iSlotNum = itemdel->iSlotNum;
|
||||
|
||||
// so, im not sure what this eIL thing does since you always delete items in inventory and not equips
|
||||
plr->Inven[itemdel->iSlotNum].iID = 0;
|
||||
plr->Inven[itemdel->iSlotNum].iType = 0;
|
||||
plr->Inven[itemdel->iSlotNum].iOpt = 0;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_DELETE_SUCC);
|
||||
}
|
||||
|
||||
static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf;
|
||||
Player* player = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (request->iSlotNum < 0 || request->iSlotNum >= AINVEN_COUNT)
|
||||
return; // sanity check
|
||||
|
||||
// gumball can only be used from inventory, so we ignore eIL
|
||||
sItemBase gumball = player->Inven[request->iSlotNum];
|
||||
sNano nano = player->Nanos[player->equippedNanos[request->iNanoSlot]];
|
||||
|
||||
// sanity check, check if gumball exists
|
||||
if (!(gumball.iOpt > 0 && gumball.iType == 7 && gumball.iID>=119 && gumball.iID<=121)) {
|
||||
std::cout << "[WARN] Gumball not found" << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
|
||||
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// sanity check, check if gumball type matches nano style
|
||||
int nanoStyle = Nanos::nanoStyle(nano.iID);
|
||||
if (!((gumball.iID == 119 && nanoStyle == 0) ||
|
||||
( gumball.iID == 120 && nanoStyle == 1) ||
|
||||
( gumball.iID == 121 && nanoStyle == 2))) {
|
||||
std::cout << "[WARN] Gumball type doesn't match nano type" << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
|
||||
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
gumball.iOpt -= 1;
|
||||
if (gumball.iOpt == 0)
|
||||
gumball = {};
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_REP_PC_ITEM_USE_SUCC) + sizeof(sSkillResult_Buff);
|
||||
|
||||
// validate response packet
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_REP_PC_ITEM_USE_SUCC), 1, sizeof(sSkillResult_Buff))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_REP_PC_ITEM_USE_SUCC packet size" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (gumball.iOpt == 0)
|
||||
gumball = {};
|
||||
|
||||
uint8_t respbuf[CN_PACKET_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 = (int32_t)SkillType::NANOSTIMPAK;
|
||||
resp->iSkillID = 144;
|
||||
|
||||
int eCSB = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
|
||||
|
||||
respdata->eCT = 1;
|
||||
respdata->iID = player->iID;
|
||||
respdata->iConditionBitFlag = CSB_FROM_ECSB(eCSB);
|
||||
|
||||
int durationMilliseconds = Abilities::SkillTable[144].durationTime[0] * 100;
|
||||
BuffStack gumballBuff = {
|
||||
durationMilliseconds / MS_PER_PLAYER_TICK,
|
||||
0,
|
||||
sock,
|
||||
BuffClass::CASH_ITEM // or BuffClass::ITEM?
|
||||
};
|
||||
player->addBuff(eCSB,
|
||||
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
|
||||
Buffs::timeBuffUpdate(self, buff, status, stack);
|
||||
},
|
||||
[](EntityRef self, Buff* buff, time_t currTime) {
|
||||
// no-op
|
||||
},
|
||||
&gumballBuff);
|
||||
|
||||
sock->sendPacket((void*)&respbuf, P_FE2CL_REP_PC_ITEM_USE_SUCC, resplen);
|
||||
// update inventory serverside
|
||||
player->Inven[resp->iSlotNum] = resp->RemainItem;
|
||||
}
|
||||
|
||||
static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// just send bank inventory
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_BANK_OPEN_SUCC, resp);
|
||||
for (int i = 0; i < ABANK_COUNT; i++) {
|
||||
resp.aBank[i] = plr->Bank[i];
|
||||
}
|
||||
resp.iExtraBank = 1;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_BANK_OPEN_SUCC);
|
||||
}
|
||||
|
||||
static void chestOpenHandler(CNSocket *sock, CNPacketData *data) {
|
||||
auto pkt = (sP_CL2FE_REQ_ITEM_CHEST_OPEN *)data->buf;
|
||||
|
||||
// sanity check
|
||||
if (pkt->eIL != 1 || pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT)
|
||||
return;
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
sItemBase *chest = &plr->Inven[pkt->iSlotNum];
|
||||
// we could reject the packet if the client thinks the item is different, but eh
|
||||
|
||||
if (chest->iType != 9) {
|
||||
std::cout << "[WARN] Player tried to open a crate with incorrect iType ?!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ACADEMY
|
||||
// check if chest isn't a nano capsule
|
||||
if (NanoCapsules.find(chest->iID) != NanoCapsules.end())
|
||||
return nanoCapsuleHandler(sock, pkt->iSlotNum, chest);
|
||||
#endif
|
||||
|
||||
// chest opening acknowledgement packet
|
||||
INITSTRUCT(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, resp);
|
||||
resp.iSlotNum = pkt->iSlotNum;
|
||||
|
||||
// item giving packet
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_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->hasBuff(ECSB_REWARD_CASH)) {
|
||||
int boost = 0;
|
||||
if (Nanos::getNanoBoost(plr)) // for gumballs
|
||||
boost = 1;
|
||||
plr->money += miscDropType.taroAmount * (5 + boost) / 25;
|
||||
}
|
||||
}
|
||||
if (rolled.fm % miscDropChance.fmDropChanceTotal < miscDropChance.fmDropChance) {
|
||||
// formula for scaling FM with player/mob level difference
|
||||
// TODO: adjust this better
|
||||
int levelDifference = plr->level - mob->level;
|
||||
int fm = miscDropType.fmAmount;
|
||||
if (levelDifference > 0)
|
||||
fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0;
|
||||
// scavenger nano boost
|
||||
if (plr->hasBuff(ECSB_REWARD_BLOB)) {
|
||||
int boost = 0;
|
||||
if (Nanos::getNanoBoost(plr)) // for gumballs
|
||||
boost = 1;
|
||||
fm += fm * (5 + boost) / 25;
|
||||
}
|
||||
|
||||
Missions::updateFusionMatter(sock, fm);
|
||||
}
|
||||
|
||||
if (rolled.potions % miscDropChance.potionDropChanceTotal < miscDropChance.potionDropChance)
|
||||
plr->batteryN += miscDropType.potionAmount;
|
||||
if (rolled.boosts % miscDropChance.boostDropChanceTotal < miscDropChance.boostDropChance)
|
||||
plr->batteryW += miscDropType.boostAmount;
|
||||
|
||||
// caps
|
||||
if (plr->batteryW > 9999)
|
||||
plr->batteryW = 9999;
|
||||
if (plr->batteryN > 9999)
|
||||
plr->batteryN = 9999;
|
||||
|
||||
// simple rewards
|
||||
reward->m_iCandy = plr->money;
|
||||
reward->m_iFusionMatter = plr->fusionmatter;
|
||||
reward->m_iBatteryN = plr->batteryN;
|
||||
reward->m_iBatteryW = plr->batteryW;
|
||||
reward->iFatigue = 100; // prevents warning message
|
||||
reward->iFatigue_Level = 1;
|
||||
reward->iItemCnt = 1; // remember to update resplen if you change this
|
||||
|
||||
int slot = findFreeSlot(plr);
|
||||
|
||||
// no drop
|
||||
if (slot == -1 || rolled.crate % crateDropChance.dropChanceTotal >= crateDropChance.dropChance) {
|
||||
// no room for an item, but you still get FM and taros
|
||||
reward->iItemCnt = 0;
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||
} else {
|
||||
// item reward
|
||||
getMobDrop(&item->sItem, crateDropChance.crateTypeDropWeights, crateDropType, rolled.crateType);
|
||||
item->iSlotNum = slot;
|
||||
item->eIL = 1; // Inventory Location. 1 means player inventory.
|
||||
|
||||
// update player
|
||||
plr->Inven[slot] = item->sItem;
|
||||
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||
}
|
||||
}
|
||||
|
||||
void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled) {
|
||||
// sanity check
|
||||
if (Items::MobToDropMap.find(mob->type) == Items::MobToDropMap.end()) {
|
||||
std::cout << "[WARN] Mob ID " << mob->type << " has no drops assigned" << std::endl;
|
||||
return;
|
||||
}
|
||||
// find mob drop id
|
||||
int mobDropId = Items::MobToDropMap[mob->type];
|
||||
|
||||
giveSingleDrop(sock, mob, mobDropId, rolled);
|
||||
|
||||
if (settings::EVENTMODE != 0) {
|
||||
// sanity check
|
||||
if (Items::EventToDropMap.find(settings::EVENTMODE) == Items::EventToDropMap.end()) {
|
||||
std::cout << "[WARN] Event " << settings::EVENTMODE << " has no mob drop assigned" << std::endl;
|
||||
return;
|
||||
}
|
||||
// find mob drop id
|
||||
int eventMobDropId = Items::EventToDropMap[settings::EVENTMODE];
|
||||
|
||||
giveSingleDrop(sock, mob, eventMobDropId, eventRolled);
|
||||
}
|
||||
}
|
||||
|
||||
void Items::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler);
|
||||
// this one is for gumballs
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_USE, itemUseHandler);
|
||||
// Bank
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BANK_OPEN, itemBankOpenHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_CHEST_OPEN, chestOpenHandler);
|
||||
}
|
||||
127
src/Items.hpp
Normal file
127
src/Items.hpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "Rand.hpp"
|
||||
#include "MobAI.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
struct CrocPotEntry {
|
||||
int multStats, multLooks;
|
||||
float base, rd0, rd1, rd2, rd3;
|
||||
};
|
||||
|
||||
struct Crate {
|
||||
int itemSetId;
|
||||
int rarityWeightId;
|
||||
};
|
||||
|
||||
struct CrateDropChance {
|
||||
int dropChance, dropChanceTotal;
|
||||
std::vector<int> crateTypeDropWeights;
|
||||
};
|
||||
|
||||
struct MiscDropChance {
|
||||
int potionDropChance, potionDropChanceTotal;
|
||||
int boostDropChance, boostDropChanceTotal;
|
||||
int taroDropChance, taroDropChanceTotal;
|
||||
int fmDropChance, fmDropChanceTotal;
|
||||
};
|
||||
|
||||
struct MiscDropType {
|
||||
int potionAmount;
|
||||
int boostAmount;
|
||||
int taroAmount;
|
||||
int fmAmount;
|
||||
};
|
||||
|
||||
struct MobDrop {
|
||||
int crateDropChanceId;
|
||||
int crateDropTypeId;
|
||||
int miscDropChanceId;
|
||||
int miscDropTypeId;
|
||||
};
|
||||
|
||||
struct ItemSet {
|
||||
// itemset-wise offswitch to rarity filtering, every crate drops every rarity (still based on rarity weights)
|
||||
bool ignoreRarity;
|
||||
// itemset-wise offswitch for gender filtering, every crate can now drop neutral/boys/girls items
|
||||
bool ignoreGender;
|
||||
// default weight of all items in the itemset
|
||||
int defaultItemWeight;
|
||||
// change the rarity class of items in the itemset here
|
||||
// rarity 0 bypasses the rarity filter for an individual item
|
||||
std::map<int, int> alterRarityMap;
|
||||
// change the gender class of items in the itemset here
|
||||
// gender 0 bypasses the gender filter for an individual item
|
||||
std::map<int, int> alterGenderMap;
|
||||
// change the item weghts items in the itemset here
|
||||
// only taken into account for chosen rarity, and if the item isn't filtered away due to gender
|
||||
std::map<int, int> alterItemWeightMap;
|
||||
std::vector<int> itemReferenceIds;
|
||||
};
|
||||
|
||||
struct ItemReference {
|
||||
int itemId;
|
||||
int type;
|
||||
int rarity;
|
||||
int gender;
|
||||
};
|
||||
|
||||
namespace Items {
|
||||
enum class SlotType {
|
||||
EQUIP = 0,
|
||||
INVENTORY = 1,
|
||||
BANK = 3
|
||||
};
|
||||
struct Item {
|
||||
bool tradeable, sellable;
|
||||
int buyPrice, sellPrice;
|
||||
int stackSize, level, rarity;
|
||||
int pointDamage, groupDamage, fireRate, defense, gender;
|
||||
int weaponType;
|
||||
// TODO: implement more as needed
|
||||
};
|
||||
struct DropRoll {
|
||||
int boosts, potions;
|
||||
int taros, fm;
|
||||
int crate, crateType;
|
||||
|
||||
DropRoll() : boosts(Rand::rand()), potions(Rand::rand()), taros(Rand::rand()), fm(Rand::rand()), crate(Rand::rand()), crateType(Rand::rand()) { }
|
||||
};
|
||||
// hopefully this is fine since it's never modified after load
|
||||
extern std::map<std::pair<int32_t, int32_t>, Item> ItemData; // <id, type> -> data
|
||||
extern std::map<int32_t, CrocPotEntry> CrocPotTable; // level gap -> entry
|
||||
extern std::map<int32_t, std::vector<int32_t>> RarityWeights;
|
||||
extern std::map<int32_t, Crate> Crates;
|
||||
extern std::map<int32_t, ItemReference> ItemReferences;
|
||||
extern std::map<std::string, std::vector<std::pair<int32_t, int32_t>>> CodeItems; // code -> vector of <id, type>
|
||||
|
||||
// mob drops
|
||||
extern std::map<int32_t, CrateDropChance> CrateDropChances;
|
||||
extern std::map<int32_t, std::vector<int32_t>> CrateDropTypes;
|
||||
extern std::map<int32_t, MiscDropChance> MiscDropChances;
|
||||
extern std::map<int32_t, MiscDropType> MiscDropTypes;
|
||||
extern std::map<int32_t, MobDrop> MobDrops;
|
||||
extern std::map<int32_t, int32_t> EventToDropMap;
|
||||
extern std::map<int32_t, int32_t> MobToDropMap;
|
||||
extern std::map<int32_t, ItemSet> ItemSets;
|
||||
|
||||
void init();
|
||||
|
||||
// mob drops
|
||||
void giveMobDrop(CNSocket *sock, Mob *mob, const DropRoll& rolled, const DropRoll& eventRolled);
|
||||
|
||||
int findFreeSlot(Player *plr);
|
||||
Item* getItemData(int32_t id, int32_t type);
|
||||
void checkItemExpire(CNSocket* sock, Player* player);
|
||||
void setItemStats(Player* plr);
|
||||
void updateEquips(CNSocket* sock, Player* plr);
|
||||
|
||||
#ifdef ACADEMY
|
||||
extern std::map<int32_t, int32_t> NanoCapsules; // crate id -> nano id
|
||||
#endif
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
661
src/Missions.cpp
Normal file
661
src/Missions.cpp
Normal file
@@ -0,0 +1,661 @@
|
||||
#include "Missions.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "Nanos.hpp"
|
||||
|
||||
using namespace Missions;
|
||||
|
||||
std::map<int32_t, Reward*> Missions::Rewards;
|
||||
std::map<int32_t, TaskData*> Missions::Tasks;
|
||||
nlohmann::json Missions::AvatarGrowth[37];
|
||||
|
||||
static void saveMission(Player* player, int missionId) {
|
||||
// sanity check missionID so we don't get exceptions
|
||||
if (missionId < 0 || missionId > 1023) {
|
||||
std::cout << "[WARN] Client submitted invalid missionId: " <<missionId<< std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Missions are stored in int64_t array
|
||||
int row = missionId / 64;
|
||||
int column = missionId % 64;
|
||||
player->aQuestFlag[row] |= (1ULL << column);
|
||||
}
|
||||
|
||||
static bool isMissionCompleted(Player* player, int missionId) {
|
||||
int row = missionId / 64;
|
||||
int column = missionId % 64;
|
||||
return player->aQuestFlag[row] & (1ULL << column);
|
||||
}
|
||||
|
||||
int Missions::findQSlot(Player *plr, int id) {
|
||||
int i;
|
||||
|
||||
// two passes. we mustn't fail to find an existing stack.
|
||||
for (i = 0; i < AQINVEN_COUNT; i++)
|
||||
if (plr->QInven[i].iID == id)
|
||||
return i;
|
||||
|
||||
// no stack. start a new one.
|
||||
for (i = 0; i < AQINVEN_COUNT; i++)
|
||||
if (plr->QInven[i].iOpt == 0)
|
||||
return i;
|
||||
|
||||
// not found
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool isQuestItemFull(CNSocket* sock, int itemId, int itemCount) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int slot = Missions::findQSlot(plr, itemId);
|
||||
if (slot == -1) {
|
||||
// this should never happen
|
||||
std::cout << "[WARN] Player has no room for quest item!?" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
return (itemCount == plr->QInven[slot].iOpt);
|
||||
}
|
||||
|
||||
static void dropQuestItem(CNSocket *sock, int task, int count, int id, int mobid) {
|
||||
std::cout << "Altered item id " << id << " by " << count << " for task id " << task << std::endl;
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_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->hasBuff(ECSB_REWARD_CASH)) { // nano boost for taros
|
||||
int boost = 0;
|
||||
if (Nanos::getNanoBoost(plr)) // for gumballs
|
||||
boost = 1;
|
||||
plr->money += reward->money * (5 + boost) / 25;
|
||||
}
|
||||
|
||||
if (plr->hasBuff(ECSB_REWARD_BLOB)) { // nano boost for fm
|
||||
int boost = 0;
|
||||
if (Nanos::getNanoBoost(plr)) // for gumballs
|
||||
boost = 1;
|
||||
updateFusionMatter(sock, reward->fusionmatter * (30 + boost) / 25);
|
||||
} else
|
||||
updateFusionMatter(sock, reward->fusionmatter);
|
||||
|
||||
// simple rewards
|
||||
resp->m_iCandy = plr->money;
|
||||
resp->m_iFusionMatter = plr->fusionmatter;
|
||||
resp->iFatigue = 100; // prevents warning message
|
||||
resp->iFatigue_Level = 1;
|
||||
resp->iItemCnt = nrewards;
|
||||
resp->m_iBatteryN = plr->batteryN;
|
||||
resp->m_iBatteryW = plr->batteryW;
|
||||
|
||||
int offset = 0;
|
||||
|
||||
// choice is actually a bitfield
|
||||
if (choice != 0)
|
||||
offset = (int)log2((int)choice);
|
||||
|
||||
for (int i = 0; i < nrewards; i++) {
|
||||
item[i].sItem.iType = reward->itemTypes[offset+i];
|
||||
item[i].sItem.iID = reward->itemIds[offset+i];
|
||||
item[i].sItem.iOpt = 1;
|
||||
item[i].iSlotNum = slots[i];
|
||||
item[i].eIL = 1;
|
||||
|
||||
// update player inventory, overwriting temporary item
|
||||
plr->Inven[slots[i]] = item[i].sItem;
|
||||
}
|
||||
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool endTask(CNSocket *sock, int32_t taskNum, int choice=0) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (Tasks.find(taskNum) == Tasks.end())
|
||||
return false;
|
||||
|
||||
// ugly pointer/reference juggling for the sake of operator overloading...
|
||||
TaskData& task = *Tasks[taskNum];
|
||||
|
||||
// sanity check
|
||||
int i;
|
||||
bool found = false;
|
||||
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == taskNum) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
return false;
|
||||
|
||||
// mission rewards
|
||||
if (Rewards.find(taskNum) != Rewards.end()) {
|
||||
if (giveMissionReward(sock, taskNum, choice) == -1)
|
||||
return false; // we don't want to send anything
|
||||
}
|
||||
// don't take away quest items if we haven't finished the quest
|
||||
|
||||
/*
|
||||
* Update player's active mission data.
|
||||
*
|
||||
* This must be done after all early returns have passed, otherwise we
|
||||
* risk introducing non-atomic changes. For example, failing to finish
|
||||
* a mission due to not having any inventory space could delete the
|
||||
* mission server-side; leading to a desync.
|
||||
*/
|
||||
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == taskNum) {
|
||||
plr->tasks[i] = 0;
|
||||
for (int j = 0; j < 3; j++) {
|
||||
plr->RemainingNPCCount[i][j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Give (or take away) quest items
|
||||
*
|
||||
* Some mission tasks give the player a quest item upon completion.
|
||||
* This is distinct from quest item mob drops.
|
||||
* They can be identified by a counter in the task indicator (ie. 1/1 Gravity Decelerator).
|
||||
* The server is responsible for dropping the correct item.
|
||||
* Yes, this is pretty stupid.
|
||||
*
|
||||
* iSUInstancename is the number of items to give. It is usually negative at the end of
|
||||
* a mission, to clean up its quest items.
|
||||
*/
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (task["m_iSUItem"][i] != 0)
|
||||
dropQuestItem(sock, taskNum, task["m_iSUInstancename"][i], task["m_iSUItem"][i], 0);
|
||||
|
||||
// if it's the last task
|
||||
if (task["m_iSUOutgoingTask"] == 0) {
|
||||
// save completed mission on player
|
||||
saveMission(plr, (int)(task["m_iHMissionID"])-1);
|
||||
|
||||
// if it's a nano mission, reward the nano.
|
||||
if (task["m_iSTNanoID"] != 0)
|
||||
Nanos::addNano(sock, task["m_iSTNanoID"], 0, true);
|
||||
|
||||
// remove current mission
|
||||
plr->CurrentMissionID = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Missions::startTask(Player* plr, int TaskID) {
|
||||
if (Missions::Tasks.find(TaskID) == Missions::Tasks.end()) {
|
||||
std::cout << "[WARN] Player submitted unknown task!?" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
TaskData& task = *Missions::Tasks[TaskID];
|
||||
|
||||
if (task["m_iCTRReqLvMin"] > plr->level) {
|
||||
std::cout << "[WARN] Player tried to start a task above their level" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isMissionCompleted(plr, (int)(task["m_iHMissionID"]) - 1)) {
|
||||
std::cout << "[WARN] Player tried to start an already completed mission" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// client freaks out if nano mission isn't sent first after relogging, so it's easiest to set it here
|
||||
if (task["m_iSTNanoID"] != 0 && plr->tasks[0] != 0) {
|
||||
// lets move task0 to different spot
|
||||
int moveToSlot = 1;
|
||||
for (; moveToSlot < ACTIVE_MISSION_COUNT; moveToSlot++)
|
||||
if (plr->tasks[moveToSlot] == 0)
|
||||
break;
|
||||
|
||||
plr->tasks[moveToSlot] = plr->tasks[0];
|
||||
plr->tasks[0] = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
plr->RemainingNPCCount[moveToSlot][i] = plr->RemainingNPCCount[0][i];
|
||||
plr->RemainingNPCCount[0][i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int i;
|
||||
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == 0) {
|
||||
plr->tasks[i] = TaskID;
|
||||
for (int j = 0; j < 3; j++) {
|
||||
plr->RemainingNPCCount[i][j] = (int)task["m_iCSUNumToKill"][j];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i == ACTIVE_MISSION_COUNT - 1 && plr->tasks[i] != TaskID) {
|
||||
std::cout << "[WARN] Player has more than 6 active missions!?" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void taskStart(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TASK_START* missionData = (sP_CL2FE_REQ_PC_TASK_START*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_SUCC, response);
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (!startTask(plr, missionData->iTaskNum)) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_FAIL, failresp);
|
||||
failresp.iTaskNum = missionData->iTaskNum;
|
||||
failresp.iErrorCode = 1; // unused in the client
|
||||
sock->sendPacket(failresp, P_FE2CL_REP_PC_TASK_START_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
TaskData& task = *Tasks[missionData->iTaskNum];
|
||||
|
||||
// Give player their delivery items at the start, or reset them to 0 at the start.
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (task["m_iSTItemID"][i] != 0)
|
||||
dropQuestItem(sock, missionData->iTaskNum, task["m_iSTItemNumNeeded"][i], task["m_iSTItemID"][i], 0);
|
||||
std::cout << "Mission requested task: " << missionData->iTaskNum << std::endl;
|
||||
response.iTaskNum = missionData->iTaskNum;
|
||||
response.iRemainTime = task["m_iSTGrantTimer"];
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
|
||||
|
||||
// if escort task, assign matching paths to all nearby NPCs
|
||||
if (task["m_iHTaskType"] == (int)eTaskTypeProperty::EscortDefence) {
|
||||
for (ChunkPos& chunkPos : Chunking::getChunksInMap(plr->instanceID)) { // check all NPCs in the instance
|
||||
Chunk* chunk = Chunking::chunks[chunkPos];
|
||||
for (EntityRef ref : chunk->entities) {
|
||||
if (ref.kind != EntityKind::PLAYER) {
|
||||
BaseNPC* npc = (BaseNPC*)ref.getEntity();
|
||||
NPCPath* path = Transport::findApplicablePath(npc->id, npc->type, missionData->iTaskNum);
|
||||
if (path != nullptr) {
|
||||
Transport::constructPathNPC(npc->id, path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void taskEnd(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TASK_END* missionData = (sP_CL2FE_REQ_PC_TASK_END*)data->buf;
|
||||
|
||||
if (Missions::Tasks.find(missionData->iTaskNum) == Missions::Tasks.end())
|
||||
return;
|
||||
|
||||
TaskData* task = Missions::Tasks[missionData->iTaskNum];
|
||||
|
||||
// handle timed mission failure
|
||||
if (task->task["m_iSTGrantTimer"] > 0 && missionData->iNPC_ID == 0) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
/*
|
||||
* Enemy killing missions
|
||||
* this is gross and should be cleaned up later
|
||||
* once we comb over mission logic more throughly
|
||||
*/
|
||||
bool mobsAreKilled = false;
|
||||
if (task->task["m_iHTaskType"] == (int)eTaskTypeProperty::Defeat) {
|
||||
mobsAreKilled = true;
|
||||
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == missionData->iTaskNum) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (plr->RemainingNPCCount[i][j] > 0) {
|
||||
mobsAreKilled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mobsAreKilled) {
|
||||
int failTaskID = task->task["m_iFOutgoingTask"];
|
||||
if (failTaskID != 0) {
|
||||
Missions::quitTask(sock, missionData->iTaskNum, false);
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
if (plr->tasks[i] == missionData->iTaskNum)
|
||||
plr->tasks[i] = failTaskID;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, response);
|
||||
|
||||
response.iTaskNum = missionData->iTaskNum;
|
||||
|
||||
if (!endTask(sock, missionData->iTaskNum, missionData->iBox1Choice)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_END_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_END_SUCC));
|
||||
}
|
||||
|
||||
static void setMission(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
sP_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID* missionData = (sP_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_SET_CURRENT_MISSION_ID, response);
|
||||
response.iCurrentMissionID = missionData->iCurrentMissionID;
|
||||
plr->CurrentMissionID = missionData->iCurrentMissionID;
|
||||
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_SET_CURRENT_MISSION_ID, sizeof(sP_FE2CL_REP_PC_SET_CURRENT_MISSION_ID));
|
||||
}
|
||||
|
||||
static void quitMission(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TASK_STOP* missionData = (sP_CL2FE_REQ_PC_TASK_STOP*)data->buf;
|
||||
quitTask(sock, missionData->iTaskNum, true);
|
||||
}
|
||||
|
||||
void Missions::quitTask(CNSocket* sock, int32_t taskNum, bool manual) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (Tasks.find(taskNum) == Tasks.end())
|
||||
return; // sanity check
|
||||
|
||||
// update player
|
||||
int i;
|
||||
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == taskNum) {
|
||||
plr->tasks[i] = 0;
|
||||
for (int j = 0; j < 3; j++) {
|
||||
plr->RemainingNPCCount[i][j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i == ACTIVE_MISSION_COUNT - 1 && plr->tasks[i] != 0) {
|
||||
std::cout << "[WARN] Player quit non-active mission!?" << std::endl;
|
||||
}
|
||||
// remove current mission
|
||||
plr->CurrentMissionID = 0;
|
||||
|
||||
TaskData& task = *Tasks[taskNum];
|
||||
|
||||
// clean up quest items
|
||||
if (manual) {
|
||||
for (i = 0; i < 3; i++) {
|
||||
if (task["m_iSUItem"][i] == 0 && task["m_iCSUItemID"][i] == 0)
|
||||
continue;
|
||||
|
||||
/*
|
||||
* It's ok to do this only server-side, because the server decides which
|
||||
* slot later items will be placed in.
|
||||
*/
|
||||
for (int j = 0; j < AQINVEN_COUNT; j++)
|
||||
if (plr->QInven[j].iID == task["m_iSUItem"][i] || plr->QInven[j].iID == task["m_iCSUItemID"][i] || plr->QInven[j].iID == task["m_iSTItemID"][i])
|
||||
memset(&plr->QInven[j], 0, sizeof(sItemBase));
|
||||
}
|
||||
} else {
|
||||
for (i = 0; i < 3; i++) {
|
||||
if (task["m_iFItemID"][i] == 0)
|
||||
continue;
|
||||
dropQuestItem(sock, taskNum, task["m_iFItemNumNeeded"][i], task["m_iFItemID"][i], 0);
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_FAIL, failResp);
|
||||
failResp.iErrorCode = 1;
|
||||
failResp.iTaskNum = taskNum;
|
||||
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_TASK_END_FAIL, sizeof(sP_FE2CL_REP_PC_TASK_END_FAIL));
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_STOP_SUCC, response);
|
||||
response.iTaskNum = taskNum;
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_STOP_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_STOP_SUCC));
|
||||
}
|
||||
|
||||
void Missions::updateFusionMatter(CNSocket* sock, int fusion) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
plr->fusionmatter += fusion;
|
||||
|
||||
// there's a much lower FM cap in the Future
|
||||
int fmCap = AvatarGrowth[plr->level]["m_iFMLimit"];
|
||||
if (plr->fusionmatter > fmCap)
|
||||
plr->fusionmatter = fmCap;
|
||||
else if (plr->fusionmatter < 0) // if somehow lowered too far
|
||||
plr->fusionmatter = 0;
|
||||
|
||||
// don't run nano mission logic at level 36
|
||||
if (plr->level >= 36)
|
||||
return;
|
||||
|
||||
// don't give the Blossom nano mission until the player's in the Past
|
||||
if (plr->level == 4 && plr->PCStyle2.iPayzoneFlag == 0)
|
||||
return;
|
||||
|
||||
// check if it is enough for the nano mission
|
||||
int fmNano = AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
|
||||
if (plr->fusionmatter < fmNano)
|
||||
return;
|
||||
|
||||
#ifndef ACADEMY
|
||||
// check if the nano task is already started
|
||||
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
TaskData& task = *Tasks[plr->tasks[i]];
|
||||
if (task["m_iSTNanoID"] != 0)
|
||||
return; // nano mission was already started!
|
||||
}
|
||||
|
||||
// start the nano mission
|
||||
startTask(plr, AvatarGrowth[plr->level]["m_iNanoQuestTaskID"]);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_SUCC, response);
|
||||
response.iTaskNum = AvatarGrowth[plr->level]["m_iNanoQuestTaskID"];
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
|
||||
#else
|
||||
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
|
||||
plr->level++;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response);
|
||||
|
||||
response.iFusionMatter = plr->fusionmatter;
|
||||
response.iLevel = plr->level;
|
||||
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, sizeof(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC));
|
||||
#endif
|
||||
|
||||
// play the beam animation for other players
|
||||
INITSTRUCT(sP_FE2CL_PC_EVENT, bcast);
|
||||
bcast.iEventID = 1; // beam effect
|
||||
bcast.iPC_ID = plr->iID;
|
||||
PlayerManager::sendToViewable(sock, (void*)&bcast, P_FE2CL_PC_EVENT, sizeof(sP_FE2CL_PC_EVENT));
|
||||
}
|
||||
|
||||
void Missions::mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
bool missionmob = false;
|
||||
|
||||
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == 0)
|
||||
continue;
|
||||
|
||||
// tasks[] should always have valid IDs
|
||||
TaskData& task = *Tasks[plr->tasks[i]];
|
||||
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (task["m_iCSUEnemyID"][j] != mobid)
|
||||
continue;
|
||||
|
||||
// acknowledge killing of mission mob...
|
||||
if (task["m_iCSUNumToKill"][j] != 0) {
|
||||
missionmob = true;
|
||||
if (plr->RemainingNPCCount[i][j] > 0) {
|
||||
plr->RemainingNPCCount[i][j]--;
|
||||
}
|
||||
}
|
||||
|
||||
// drop quest item
|
||||
if (task["m_iCSUItemNumNeeded"][j] != 0 && !isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j]) ) {
|
||||
bool drop = rolls[plr->tasks[i]] % 100 < task["m_iSTItemDropRate"][j];
|
||||
if (drop) {
|
||||
dropQuestItem(sock, plr->tasks[i], 1, task["m_iCSUItemID"][j], mobid);
|
||||
|
||||
/*
|
||||
* Workaround: The client has a bug where it only sends a TASK_END request
|
||||
* for the first task of multiple that met their quest item requirements
|
||||
* at the same time. We deal with this by sending TASK_END response packets
|
||||
* proactively and then silently ignoring the extra TASK_END requests it
|
||||
* sends afterwards.
|
||||
*/
|
||||
if (isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j])) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, end);
|
||||
end.iTaskNum = plr->tasks[i];
|
||||
|
||||
if (!endTask(sock, plr->tasks[i]))
|
||||
continue;
|
||||
|
||||
sock->sendPacket(end, P_FE2CL_REP_PC_TASK_END_SUCC);
|
||||
}
|
||||
} else {
|
||||
// fail to drop (itemID == 0)
|
||||
dropQuestItem(sock, plr->tasks[i], 1, 0, mobid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ...but only once
|
||||
// XXX: is it actually necessary to do it this way?
|
||||
if (missionmob) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC, kill);
|
||||
|
||||
kill.iNPCID = mobid;
|
||||
|
||||
sock->sendPacket((void*)&kill, P_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC, sizeof(sP_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC));
|
||||
}
|
||||
}
|
||||
|
||||
void Missions::failInstancedMissions(CNSocket* sock) {
|
||||
// loop through all tasks; if the required instance is being left, "fail" the task
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int taskNum = plr->tasks[i];
|
||||
if (Missions::Tasks.find(taskNum) == Missions::Tasks.end())
|
||||
continue; // sanity check
|
||||
|
||||
TaskData* task = Missions::Tasks[taskNum];
|
||||
if (task->task["m_iRequireInstanceID"] != 0) { // mission is instanced
|
||||
int failTaskID = task->task["m_iFOutgoingTask"];
|
||||
if (failTaskID != 0) {
|
||||
Missions::quitTask(sock, taskNum, false);
|
||||
//plr->tasks[i] = failTaskID; // this causes the client to freak out and send a dupe task
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Missions::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_START, taskStart);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_END, taskEnd);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID, setMission);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_STOP, quitMission);
|
||||
}
|
||||
57
src/Missions.hpp
Normal file
57
src/Missions.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "JSON.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
|
||||
#include <map>
|
||||
|
||||
struct Reward {
|
||||
int32_t id;
|
||||
int32_t itemTypes[4];
|
||||
int32_t itemIds[4];
|
||||
int32_t money;
|
||||
int32_t fusionmatter;
|
||||
|
||||
Reward(int32_t id, nlohmann::json types, nlohmann::json ids, int32_t m, int32_t fm) :
|
||||
id(id), money(m), fusionmatter(fm) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
itemTypes[i] = types[i];
|
||||
itemIds[i] = ids[i];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
struct TaskData {
|
||||
/*
|
||||
* TODO: We'll probably want to keep only the data the server actually needs,
|
||||
* but for now RE/development is much easier if we have everything at
|
||||
* our fingertips.
|
||||
*/
|
||||
nlohmann::json task;
|
||||
|
||||
TaskData(nlohmann::json t) : task(t) {}
|
||||
|
||||
// convenience
|
||||
auto operator[](std::string s) { return task[s]; }
|
||||
};
|
||||
|
||||
namespace Missions {
|
||||
extern std::map<int32_t, Reward*> Rewards;
|
||||
extern std::map<int32_t, TaskData*> Tasks;
|
||||
extern nlohmann::json AvatarGrowth[37];
|
||||
void init();
|
||||
int findQSlot(Player *plr, int id);
|
||||
|
||||
bool startTask(Player* plr, int TaskID);
|
||||
|
||||
// checks if player doesn't have n/n quest items
|
||||
void updateFusionMatter(CNSocket* sock, int fusion);
|
||||
|
||||
void mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls);
|
||||
|
||||
void quitTask(CNSocket* sock, int32_t taskNum, bool manual);
|
||||
|
||||
void failInstancedMissions(CNSocket* sock);
|
||||
}
|
||||
854
src/MobAI.cpp
Normal file
854
src/MobAI.cpp
Normal file
@@ -0,0 +1,854 @@
|
||||
#include "MobAI.hpp"
|
||||
|
||||
#include "Chunking.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
#include "Entities.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Racing.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "Abilities.hpp"
|
||||
#include "Combat.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Rand.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <limits.h>
|
||||
|
||||
using namespace MobAI;
|
||||
|
||||
bool MobAI::simulateMobs = settings::SIMULATEMOBS;
|
||||
|
||||
void Mob::step(time_t currTime) {
|
||||
if (playersInView < 0)
|
||||
std::cout << "[WARN] Weird playerview value " << playersInView << std::endl;
|
||||
|
||||
// skip movement and combat if disabled or not in view
|
||||
if ((!MobAI::simulateMobs || playersInView == 0) && state != AIState::DEAD
|
||||
&& state != AIState::RETREAT)
|
||||
return;
|
||||
|
||||
// call superclass step
|
||||
CombatNPC::step(currTime);
|
||||
}
|
||||
|
||||
int Mob::takeDamage(EntityRef src, int amt) {
|
||||
|
||||
// cannot kill mobs multiple times; cannot harm retreating mobs
|
||||
if (state != AIState::ROAMING && state != AIState::COMBAT) {
|
||||
return 0; // no damage
|
||||
}
|
||||
|
||||
if (skillStyle >= 0)
|
||||
return 0; // don't hurt a mob casting corruption
|
||||
|
||||
if (state == AIState::ROAMING) {
|
||||
assert(target == nullptr && src.kind == EntityKind::PLAYER); // TODO: players only for now
|
||||
transition(AIState::COMBAT, src);
|
||||
|
||||
if (groupLeader != 0)
|
||||
MobAI::followToCombat(this);
|
||||
}
|
||||
|
||||
// wake up sleeping monster
|
||||
if (hasBuff(ECSB_MEZ)) {
|
||||
removeBuff(ECSB_MEZ);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
|
||||
pkt1.eCT = 2;
|
||||
pkt1.iID = id;
|
||||
pkt1.iConditionBitFlag = getCompositeCondition();
|
||||
NPCManager::sendToViewable(this, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
||||
}
|
||||
|
||||
// call superclass takeDamage
|
||||
return CombatNPC::takeDamage(src, amt);
|
||||
}
|
||||
|
||||
/*
|
||||
* Dynamic lerp; distinct from Transport::lerp(). This one doesn't care about height and
|
||||
* only returns the first step, since the rest will need to be recalculated anyway if chasing player.
|
||||
*/
|
||||
static std::pair<int,int> lerp(int x1, int y1, int x2, int y2, int speed) {
|
||||
std::pair<int,int> ret = {x1, y1};
|
||||
|
||||
if (speed == 0)
|
||||
return ret;
|
||||
|
||||
int distance = hypot(x1 - x2, y1 - y2);
|
||||
|
||||
if (distance > speed) {
|
||||
|
||||
int lerps = distance / speed;
|
||||
|
||||
// interpolate only the first point
|
||||
float frac = 1.0f / lerps;
|
||||
|
||||
ret.first = (x1 + (x2 - x1) * frac);
|
||||
ret.second = (y1 + (y2 - y1) * frac);
|
||||
} else {
|
||||
ret.first = x2;
|
||||
ret.second = y2;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void MobAI::clearDebuff(Mob *mob) {
|
||||
mob->skillStyle = -1;
|
||||
mob->clearBuffs(false);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
|
||||
pkt1.eCT = 2;
|
||||
pkt1.iID = mob->id;
|
||||
pkt1.iConditionBitFlag = mob->getCompositeCondition();
|
||||
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
||||
}
|
||||
|
||||
void MobAI::followToCombat(Mob *mob) {
|
||||
if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->kind == EntityKind::MOB) {
|
||||
Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (leadMob->groupMember[i] == 0)
|
||||
break;
|
||||
|
||||
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->kind != EntityKind::MOB) {
|
||||
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
|
||||
continue;
|
||||
}
|
||||
Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]];
|
||||
|
||||
if (followerMob->state != AIState::ROAMING) // only roaming mobs should transition to combat
|
||||
continue;
|
||||
|
||||
followerMob->transition(AIState::COMBAT, mob->target);
|
||||
}
|
||||
|
||||
if (leadMob->state != AIState::ROAMING)
|
||||
return;
|
||||
|
||||
leadMob->transition(AIState::COMBAT, mob->target);
|
||||
}
|
||||
}
|
||||
|
||||
void MobAI::groupRetreat(Mob *mob) {
|
||||
if (NPCManager::NPCs.find(mob->groupLeader) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupLeader]->kind != EntityKind::MOB)
|
||||
return;
|
||||
|
||||
Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (leadMob->groupMember[i] == 0)
|
||||
break;
|
||||
|
||||
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->kind != EntityKind::MOB) {
|
||||
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
|
||||
continue;
|
||||
}
|
||||
Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]];
|
||||
|
||||
if (followerMob->state != AIState::COMBAT)
|
||||
continue;
|
||||
|
||||
followerMob->target = nullptr;
|
||||
followerMob->state = AIState::RETREAT;
|
||||
clearDebuff(followerMob);
|
||||
}
|
||||
|
||||
if (leadMob->state != AIState::COMBAT)
|
||||
return;
|
||||
|
||||
leadMob->target = nullptr;
|
||||
leadMob->state = AIState::RETREAT;
|
||||
clearDebuff(leadMob);
|
||||
}
|
||||
|
||||
/*
|
||||
* Aggro on nearby players.
|
||||
* Even if they're in range, we can't assume they're all in the same one chunk
|
||||
* as the mob, since it might be near a chunk boundary.
|
||||
*/
|
||||
bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
|
||||
CNSocket *closest = nullptr;
|
||||
int closestDistance = INT_MAX;
|
||||
|
||||
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
|
||||
Chunk* chunk = *it;
|
||||
for (const EntityRef& ref : chunk->entities) {
|
||||
// TODO: support targetting other CombatNPCs
|
||||
if (ref.kind != EntityKind::PLAYER)
|
||||
continue;
|
||||
|
||||
CNSocket *s = ref.sock;
|
||||
Player *plr = PlayerManager::getPlayer(s);
|
||||
|
||||
if (plr->HP <= 0 || plr->onMonkey)
|
||||
continue;
|
||||
|
||||
int mobRange = mob->sightRange;
|
||||
|
||||
if (plr->hasBuff(ECSB_UP_STEALTH)
|
||||
|| Racing::EPRaces.find(s) != Racing::EPRaces.end())
|
||||
mobRange /= 3;
|
||||
|
||||
// 0.33x - 1.66x the range
|
||||
int levelDifference = plr->level - mob->level;
|
||||
if (levelDifference > -10)
|
||||
mobRange = levelDifference < 10 ? mobRange - (levelDifference * mobRange / 15) : mobRange / 3;
|
||||
|
||||
if (mob->state != AIState::ROAMING && plr->inCombat) // freshly out of aggro mobs
|
||||
mobRange = mob->sightRange * 2; // should not be impacted by the above
|
||||
|
||||
if (plr->iSpecialState & (CN_SPECIAL_STATE_FLAG__INVISIBLE|CN_SPECIAL_STATE_FLAG__INVULNERABLE))
|
||||
mobRange = -1;
|
||||
|
||||
// height is relevant for aggro distance because of platforming
|
||||
int xyDistance = hypot(mob->x - plr->x, mob->y - plr->y);
|
||||
int distance = hypot(xyDistance, (mob->z - plr->z) * 2); // difference in Z counts twice
|
||||
|
||||
if (distance > mobRange || distance > closestDistance)
|
||||
continue;
|
||||
|
||||
// found a player
|
||||
closest = s;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
if (closest != nullptr) {
|
||||
// found closest player. engage.
|
||||
mob->transition(AIState::COMBAT, closest);
|
||||
|
||||
if (mob->groupLeader != 0)
|
||||
followToCombat(mob);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int mobStyle) {
|
||||
Player *plr = PlayerManager::getPlayer(mob->target);
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT) + targetData[0] * sizeof(sCAttackResult);
|
||||
|
||||
// validate response packet
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT), targetData[0], sizeof(sCAttackResult))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_CORRUPTION_HIT packet size" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t respbuf[CN_PACKET_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->id;
|
||||
resp->iSkillID = skillID;
|
||||
resp->iStyle = mobStyle;
|
||||
resp->iValue1 = plr->x;
|
||||
resp->iValue2 = plr->y;
|
||||
resp->iValue3 = plr->z;
|
||||
resp->iTargetCnt = targetData[0];
|
||||
|
||||
for (int i = 0; i < targetData[0]; i++) {
|
||||
CNSocket *sock = nullptr;
|
||||
Player *plr = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == targetData[i+1]) {
|
||||
sock = pair.first;
|
||||
plr = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
if (plr == nullptr) {
|
||||
std::cout << "[WARN] dealCorruption: player ID not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
respdata[i].eCT = 1;
|
||||
respdata[i].iID = plr->iID;
|
||||
respdata[i].bProtected = 0;
|
||||
|
||||
respdata[i].iActiveNanoSlotNum = -1;
|
||||
for (int n = 0; n < 3; n++)
|
||||
if (plr->activeNano == plr->equippedNanos[n])
|
||||
respdata[i].iActiveNanoSlotNum = n;
|
||||
respdata[i].iNanoID = plr->activeNano;
|
||||
|
||||
int nanoStyle = Nanos::nanoStyle(plr->activeNano);
|
||||
if (nanoStyle == -1) { // no nano
|
||||
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
|
||||
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
|
||||
} else if (mobStyle == nanoStyle) {
|
||||
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
|
||||
respdata[i].iDamage = 0;
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
|
||||
} else if (mobStyle - nanoStyle == 1 || nanoStyle - mobStyle == 2) {
|
||||
respdata[i].iHitFlag = HF_BIT_STYLE_WIN;
|
||||
respdata[i].iDamage = 0;
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45;
|
||||
if (plr->Nanos[plr->activeNano].iStamina > 150)
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 150;
|
||||
// fire damage power disguised as a corruption attack back at the enemy
|
||||
SkillData skill = {
|
||||
SkillType::DAMAGE, // skillType
|
||||
SkillEffectTarget::POINT, // effectTarget
|
||||
1, // effectType
|
||||
SkillTargetType::MOBS, // targetType
|
||||
SkillDrainType::ACTIVE, // drainType
|
||||
0, // effectArea
|
||||
{0, 0, 0, 0}, // batteryUse
|
||||
{0, 0, 0, 0}, // durationTime
|
||||
{0, 0, 0}, // valueTypes (unused)
|
||||
{
|
||||
{200, 200, 200, 200},
|
||||
{200, 200, 200, 200},
|
||||
{200, 200, 200, 200},
|
||||
}
|
||||
};
|
||||
Abilities::useNanoSkill(sock, &skill, *plr->getActiveNano(), { mob });
|
||||
} else {
|
||||
respdata[i].iHitFlag = HF_BIT_STYLE_LOSE;
|
||||
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina -= 90;
|
||||
if (plr->Nanos[plr->activeNano].iStamina < 0) {
|
||||
respdata[i].bNanoDeactive = 1;
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
|
||||
plr->HP -= respdata[i].iDamage;
|
||||
|
||||
respdata[i].iHP = plr->HP;
|
||||
respdata[i].iConditionBitFlag = plr->getCompositeCondition();
|
||||
|
||||
if (plr->HP <= 0) {
|
||||
if (!MobAI::aggroCheck(mob, getTime()))
|
||||
mob->transition(AIState::RETREAT, mob->target);
|
||||
}
|
||||
}
|
||||
|
||||
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_CORRUPTION_HIT, resplen);
|
||||
}
|
||||
|
||||
static void useAbilities(Mob *mob, time_t currTime) {
|
||||
Player *plr = PlayerManager::getPlayer(mob->target);
|
||||
|
||||
if (mob->skillStyle >= 0) { // corruption hit
|
||||
int skillID = (int)mob->data["m_iCorruptionType"];
|
||||
std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
|
||||
int temp = mob->skillStyle;
|
||||
mob->skillStyle = -3; // corruption cooldown
|
||||
mob->nextAttack = currTime + 1000;
|
||||
dealCorruption(mob, targetData, skillID, temp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mob->skillStyle == -2) { // eruption hit
|
||||
int skillID = (int)mob->data["m_iMegaType"];
|
||||
std::vector<ICombatant*> targets{};
|
||||
|
||||
// find the players within range of eruption
|
||||
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
|
||||
Chunk* chunk = *it;
|
||||
for (const EntityRef& ref : chunk->entities) {
|
||||
// TODO: see aggroCheck()
|
||||
if (ref.kind != EntityKind::PLAYER)
|
||||
continue;
|
||||
|
||||
CNSocket *s = ref.sock;
|
||||
Player *plr = PlayerManager::getPlayer(s);
|
||||
|
||||
if (!plr->isAlive())
|
||||
continue;
|
||||
|
||||
int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y);
|
||||
if (distance < Abilities::SkillTable[skillID].effectArea) {
|
||||
targets.push_back(plr);
|
||||
if (targets.size() > 3) // make sure not to have more than 4
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Abilities::useNPCSkill(mob->id, skillID, targets);
|
||||
mob->skillStyle = -3; // eruption cooldown
|
||||
mob->nextAttack = currTime + 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mob->skillStyle == -3) { // cooldown expires
|
||||
mob->skillStyle = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
int random = Rand::rand(2000) * 1000;
|
||||
int prob1 = (int)mob->data["m_iActiveSkill1Prob"]; // active skill probability
|
||||
int prob2 = (int)mob->data["m_iCorruptionTypeProb"]; // corruption probability
|
||||
int prob3 = (int)mob->data["m_iMegaTypeProb"]; // eruption probability
|
||||
|
||||
if (random < prob1) { // active skill hit
|
||||
int skillID = (int)mob->data["m_iActiveSkill1"];
|
||||
SkillData* skill = &Abilities::SkillTable[skillID];
|
||||
int debuffID = Abilities::getCSTBFromST(skill->skillType);
|
||||
if(plr->hasBuff(debuffID))
|
||||
return; // prevent debuffing a player twice
|
||||
Abilities::useNPCSkill(mob->getRef(), skillID, { plr });
|
||||
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
|
||||
return;
|
||||
}
|
||||
|
||||
if (random < prob1 + prob2) { // corruption windup
|
||||
int skillID = (int)mob->data["m_iCorruptionType"];
|
||||
INITSTRUCT(sP_FE2CL_NPC_SKILL_CORRUPTION_READY, pkt);
|
||||
pkt.iNPC_ID = mob->id;
|
||||
pkt.iSkillID = skillID;
|
||||
pkt.iValue1 = plr->x;
|
||||
pkt.iValue2 = plr->y;
|
||||
pkt.iValue3 = plr->z;
|
||||
mob->skillStyle = Nanos::nanoStyle(plr->activeNano) - 1;
|
||||
if (mob->skillStyle == -1)
|
||||
mob->skillStyle = 2;
|
||||
if (mob->skillStyle == -2)
|
||||
mob->skillStyle = Rand::rand(3);
|
||||
pkt.iStyle = mob->skillStyle;
|
||||
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_SKILL_CORRUPTION_READY, sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_READY));
|
||||
mob->nextAttack = currTime + 1800;
|
||||
return;
|
||||
}
|
||||
|
||||
if (random < prob1 + prob2 + prob3) { // eruption windup
|
||||
int skillID = (int)mob->data["m_iMegaType"];
|
||||
INITSTRUCT(sP_FE2CL_NPC_SKILL_READY, pkt);
|
||||
pkt.iNPC_ID = mob->id;
|
||||
pkt.iSkillID = skillID;
|
||||
pkt.iValue1 = mob->hitX = plr->x;
|
||||
pkt.iValue2 = mob->hitY = plr->y;
|
||||
pkt.iValue3 = mob->hitZ = plr->z;
|
||||
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_SKILL_READY, sizeof(sP_FE2CL_NPC_SKILL_READY));
|
||||
mob->nextAttack = currTime + 1800;
|
||||
mob->skillStyle = -2;
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void MobAI::incNextMovement(Mob* mob, time_t currTime) {
|
||||
if (currTime == 0)
|
||||
currTime = getTime();
|
||||
|
||||
int delay = (int)mob->data["m_iDelayTime"] * 1000;
|
||||
mob->nextMovement = currTime + delay / 2 + Rand::rand(delay / 2);
|
||||
}
|
||||
|
||||
void MobAI::deadStep(CombatNPC* npc, time_t currTime) {
|
||||
Mob* self = (Mob*)npc;
|
||||
|
||||
// despawn the mob after a short delay
|
||||
if (self->killedTime != 0 && !self->despawned && currTime - self->killedTime > 2000) {
|
||||
self->despawned = true;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
|
||||
|
||||
pkt.iNPC_ID = self->id;
|
||||
|
||||
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
|
||||
|
||||
// if it was summoned, mark it for removal
|
||||
if (self->summoned) {
|
||||
std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl;
|
||||
NPCManager::queueNPCRemoval(self->id);
|
||||
return;
|
||||
}
|
||||
|
||||
// pre-set spawn coordinates if not marked for removal
|
||||
self->x = self->spawnX;
|
||||
self->y = self->spawnY;
|
||||
self->z = self->spawnZ;
|
||||
}
|
||||
|
||||
// to guide their groupmates, group leaders still need to move despite being dead
|
||||
if (self->groupLeader == self->id)
|
||||
roamingStep(self, currTime);
|
||||
|
||||
if (self->killedTime != 0 && currTime - self->killedTime < self->regenTime * 100)
|
||||
return;
|
||||
|
||||
std::cout << "respawning mob " << self->id << " with HP = " << self->maxHealth << std::endl;
|
||||
|
||||
self->transition(AIState::ROAMING, self->id);
|
||||
|
||||
// if mob is a group leader/follower, spawn where the group is.
|
||||
if (self->groupLeader != 0) {
|
||||
if (NPCManager::NPCs.find(self->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[self->groupLeader]->kind == EntityKind::MOB) {
|
||||
Mob* leaderMob = (Mob*)NPCManager::NPCs[self->groupLeader];
|
||||
self->x = leaderMob->x + self->offsetX;
|
||||
self->y = leaderMob->y + self->offsetY;
|
||||
self->z = leaderMob->z;
|
||||
} else {
|
||||
std::cout << "[WARN] deadStep: mob cannot find it's leader!" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_NPC_NEW, pkt);
|
||||
|
||||
pkt.NPCAppearanceData = self->getAppearanceData();
|
||||
|
||||
// notify all nearby players
|
||||
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
|
||||
}
|
||||
|
||||
void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
|
||||
Mob* self = (Mob*)npc;
|
||||
assert(self->target != nullptr);
|
||||
|
||||
// lose aggro if the player lost connection
|
||||
if (PlayerManager::players.find(self->target) == PlayerManager::players.end()) {
|
||||
if (!MobAI::aggroCheck(self, getTime()))
|
||||
self->transition(AIState::RETREAT, self->target);
|
||||
return;
|
||||
}
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(self->target);
|
||||
|
||||
// lose aggro if the player became invulnerable or died
|
||||
if (plr->HP <= 0
|
||||
|| (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) {
|
||||
if (!MobAI::aggroCheck(self, getTime()))
|
||||
self->transition(AIState::RETREAT, self->target);
|
||||
return;
|
||||
}
|
||||
|
||||
// tick buffs
|
||||
auto it = npc->buffs.begin();
|
||||
while(it != npc->buffs.end()) {
|
||||
Buff* buff = (*it).second;
|
||||
buff->combatTick(currTime);
|
||||
|
||||
// if mob state changed, end the step
|
||||
if(self->state != AIState::COMBAT)
|
||||
return;
|
||||
|
||||
buff->tick(currTime);
|
||||
if(buff->isStale()) {
|
||||
// garbage collect
|
||||
it = npc->buffs.erase(it);
|
||||
delete buff;
|
||||
}
|
||||
else it++;
|
||||
}
|
||||
|
||||
// skip attack if stunned or asleep
|
||||
if (self->hasBuff(ECSB_STUN) || self->hasBuff(ECSB_MEZ)) {
|
||||
self->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
|
||||
return;
|
||||
}
|
||||
|
||||
int distance = hypot(plr->x - self->x, plr->y - self->y);
|
||||
int mobRange = (int)self->data["m_iAtkRange"] + (int)self->data["m_iRadius"];
|
||||
|
||||
if (currTime >= self->nextAttack) {
|
||||
if (self->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance.
|
||||
useAbilities(self, currTime);
|
||||
if (self->target == nullptr)
|
||||
return;
|
||||
}
|
||||
|
||||
int distanceToTravel = INT_MAX;
|
||||
int speed = self->speed;
|
||||
// movement logic: move when out of range but don't move while casting a skill
|
||||
if (distance > mobRange && self->skillStyle == -1) {
|
||||
if (self->nextMovement != 0 && currTime < self->nextMovement)
|
||||
return;
|
||||
self->nextMovement = currTime + 400;
|
||||
if (currTime >= self->nextAttack)
|
||||
self->nextAttack = 0;
|
||||
|
||||
// halve movement speed if snared
|
||||
if (self->hasBuff(ECSB_DN_MOVE_SPEED))
|
||||
speed /= 2;
|
||||
|
||||
int targetX = plr->x;
|
||||
int targetY = plr->y;
|
||||
if (self->groupLeader != 0) {
|
||||
targetX += self->offsetX*distance/(self->idleRange + 1);
|
||||
targetY += self->offsetY*distance/(self->idleRange + 1);
|
||||
}
|
||||
|
||||
distanceToTravel = std::min(distance-mobRange+1, speed*2/5);
|
||||
auto targ = lerp(self->x, self->y, targetX, targetY, distanceToTravel);
|
||||
if (distanceToTravel < speed*2/5 && currTime >= self->nextAttack)
|
||||
self->nextAttack = 0;
|
||||
|
||||
NPCManager::updateNPCPosition(self->id, targ.first, targ.second, self->z, self->instanceID, self->angle);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
||||
|
||||
pkt.iNPC_ID = self->id;
|
||||
pkt.iSpeed = speed;
|
||||
pkt.iToX = self->x = targ.first;
|
||||
pkt.iToY = self->y = targ.second;
|
||||
pkt.iToZ = plr->z;
|
||||
pkt.iMoveStyle = 1;
|
||||
|
||||
// notify all nearby players
|
||||
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
||||
}
|
||||
|
||||
/* attack logic
|
||||
* 2/5 represents 400 ms which is the time interval mobs use per movement logic step
|
||||
* if the mob is one move interval away, we should just start attacking anyways.
|
||||
*/
|
||||
if (distance <= mobRange || distanceToTravel < self->speed*2/5) {
|
||||
if (self->nextAttack == 0 || currTime >= self->nextAttack) {
|
||||
self->nextAttack = currTime + (int)self->data["m_iDelayTime"] * 100;
|
||||
Combat::npcAttackPc(self, currTime);
|
||||
}
|
||||
}
|
||||
|
||||
// retreat if the player leaves combat range
|
||||
int xyDistance = hypot(plr->x - self->roamX, plr->y - self->roamY);
|
||||
distance = hypot(xyDistance, plr->z - self->roamZ);
|
||||
if (distance >= self->data["m_iCombatRange"]) {
|
||||
self->transition(AIState::RETREAT, self->target);
|
||||
}
|
||||
}
|
||||
|
||||
void MobAI::roamingStep(CombatNPC* npc, time_t currTime) {
|
||||
Mob* self = (Mob*)npc;
|
||||
|
||||
/*
|
||||
* We reuse nextAttack to avoid scanning for players all the time, but to still
|
||||
* do so more often than if we waited for nextMovement (which is way too slow).
|
||||
* In the case of group leaders, this step will be called by dead mobs, so disable attack.
|
||||
*/
|
||||
if (self->state != AIState::DEAD && (self->nextAttack == 0 || currTime >= self->nextAttack)) {
|
||||
self->nextAttack = currTime + 500;
|
||||
if (aggroCheck(self, currTime))
|
||||
return;
|
||||
}
|
||||
|
||||
// no random roaming if the mob already has a set path
|
||||
if (self->staticPath)
|
||||
return;
|
||||
|
||||
if (self->groupLeader != 0 && self->groupLeader != self->id) // don't roam by yourself without group leader
|
||||
return;
|
||||
|
||||
/*
|
||||
* mob->nextMovement is also updated whenever the path queue is traversed in
|
||||
* Transport::stepNPCPathing() (which ticks at a higher frequency than nextMovement),
|
||||
* so we don't have to check if there's already entries in the queue since we know there won't be.
|
||||
*/
|
||||
if (self->nextMovement != 0 && currTime < self->nextMovement)
|
||||
return;
|
||||
incNextMovement(self, currTime);
|
||||
|
||||
int xStart = self->spawnX - self->idleRange/2;
|
||||
int yStart = self->spawnY - self->idleRange/2;
|
||||
|
||||
// some mobs don't move (and we mustn't divide/modulus by zero)
|
||||
if (self->idleRange == 0 || self->speed == 0)
|
||||
return;
|
||||
|
||||
int farX, farY, distance;
|
||||
int minDistance = self->idleRange / 2;
|
||||
|
||||
// pick a random destination
|
||||
farX = xStart + Rand::rand(self->idleRange);
|
||||
farY = yStart + Rand::rand(self->idleRange);
|
||||
|
||||
distance = std::abs(std::max(farX - self->x, farY - self->y));
|
||||
if (distance == 0)
|
||||
distance += 1; // hack to avoid FPE
|
||||
|
||||
// if it's too short a walk, go further in that direction
|
||||
farX = self->x + (farX - self->x) * minDistance / distance;
|
||||
farY = self->y + (farY - self->y) * minDistance / distance;
|
||||
|
||||
// but don't got out of bounds
|
||||
farX = std::clamp(farX, xStart, xStart + self->idleRange);
|
||||
farY = std::clamp(farY, yStart, yStart + self->idleRange);
|
||||
|
||||
// halve movement speed if snared
|
||||
if (self->hasBuff(ECSB_DN_MOVE_SPEED))
|
||||
self->speed /= 2;
|
||||
|
||||
std::queue<Vec3> queue;
|
||||
Vec3 from = { self->x, self->y, self->z };
|
||||
Vec3 to = { farX, farY, self->z };
|
||||
|
||||
// add a route to the queue; to be processed in Transport::stepNPCPathing()
|
||||
Transport::lerp(&queue, from, to, self->speed);
|
||||
Transport::NPCQueues[self->id] = queue;
|
||||
|
||||
if (self->groupLeader != 0 && self->groupLeader == self->id) {
|
||||
// make followers follow this npc.
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (self->groupMember[i] == 0)
|
||||
break;
|
||||
|
||||
if (NPCManager::NPCs.find(self->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[self->groupMember[i]]->kind != EntityKind::MOB) {
|
||||
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::queue<Vec3> queue2;
|
||||
Mob* followerMob = (Mob*)NPCManager::NPCs[self->groupMember[i]];
|
||||
from = { followerMob->x, followerMob->y, followerMob->z };
|
||||
to = { farX + followerMob->offsetX, farY + followerMob->offsetY, followerMob->z };
|
||||
Transport::lerp(&queue2, from, to, self->speed);
|
||||
Transport::NPCQueues[followerMob->id] = queue2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MobAI::retreatStep(CombatNPC* npc, time_t currTime) {
|
||||
Mob* self = (Mob*)npc;
|
||||
|
||||
if (self->nextMovement != 0 && currTime < self->nextMovement)
|
||||
return;
|
||||
|
||||
self->nextMovement = currTime + 400;
|
||||
|
||||
// distance between spawn point and current location
|
||||
int distance = hypot(self->x - self->roamX, self->y - self->roamY);
|
||||
|
||||
//if (distance > mob->data["m_iIdleRange"]) {
|
||||
if (distance > 10) {
|
||||
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
||||
|
||||
auto targ = lerp(self->x, self->y, self->roamX, self->roamY, (int)self->speed*4/5);
|
||||
|
||||
pkt.iNPC_ID = self->id;
|
||||
pkt.iSpeed = (int)self->speed * 2;
|
||||
pkt.iToX = self->x = targ.first;
|
||||
pkt.iToY = self->y = targ.second;
|
||||
pkt.iToZ = self->z = self->spawnZ;
|
||||
pkt.iMoveStyle = 1;
|
||||
|
||||
// notify all nearby players
|
||||
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
||||
}
|
||||
|
||||
// if we got there
|
||||
//if (distance <= mob->data["m_iIdleRange"]) {
|
||||
if (distance <= 10) { // retreat back to the spawn point
|
||||
self->transition(AIState::ROAMING, self->id);
|
||||
}
|
||||
}
|
||||
|
||||
void MobAI::onRoamStart(CombatNPC* npc, EntityRef src) {
|
||||
Mob* self = (Mob*)npc;
|
||||
|
||||
self->hp = self->maxHealth;
|
||||
self->killedTime = 0;
|
||||
self->nextAttack = 0;
|
||||
|
||||
// cast a return home heal spell, this is the right way(tm)
|
||||
Abilities::useNPCSkill(npc->getRef(), 110, { npc });
|
||||
|
||||
// clear outlying debuffs
|
||||
clearDebuff(self);
|
||||
}
|
||||
|
||||
void MobAI::onCombatStart(CombatNPC* npc, EntityRef src) {
|
||||
Mob* self = (Mob*)npc;
|
||||
|
||||
assert(src.kind == EntityKind::PLAYER);
|
||||
self->target = src.sock;
|
||||
self->nextMovement = getTime();
|
||||
self->nextAttack = 0;
|
||||
|
||||
self->roamX = self->x;
|
||||
self->roamY = self->y;
|
||||
self->roamZ = self->z;
|
||||
|
||||
int skillID = (int)self->data["m_iPassiveBuff"];
|
||||
if(skillID != 0) // cast passive
|
||||
Abilities::useNPCSkill(npc->getRef(), skillID, { npc });
|
||||
}
|
||||
|
||||
void MobAI::onRetreat(CombatNPC* npc, EntityRef src) {
|
||||
Mob* self = (Mob*)npc;
|
||||
|
||||
self->target = nullptr;
|
||||
MobAI::clearDebuff(self);
|
||||
if (self->groupLeader != 0)
|
||||
MobAI::groupRetreat(self);
|
||||
}
|
||||
|
||||
void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
|
||||
Mob* self = (Mob*)npc;
|
||||
|
||||
self->target = nullptr;
|
||||
self->skillStyle = -1;
|
||||
self->clearBuffs(true);
|
||||
self->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
|
||||
|
||||
// check for the edge case where hitting the mob did not aggro it
|
||||
if (src.kind == EntityKind::PLAYER && src.isValid()) {
|
||||
Player* plr = PlayerManager::getPlayer(src.sock);
|
||||
|
||||
Items::DropRoll rolled;
|
||||
Items::DropRoll eventRolled;
|
||||
std::map<int, int> qitemRolls;
|
||||
std::vector<Player*> playerRefs;
|
||||
|
||||
if (plr->group == nullptr) {
|
||||
playerRefs.push_back(plr);
|
||||
Combat::genQItemRolls(playerRefs, qitemRolls);
|
||||
Items::giveMobDrop(src.sock, self, rolled, eventRolled);
|
||||
Missions::mobKilled(src.sock, self->type, qitemRolls);
|
||||
}
|
||||
else {
|
||||
auto players = plr->group->filter(EntityKind::PLAYER);
|
||||
for (EntityRef pRef : players) playerRefs.push_back(PlayerManager::getPlayer(pRef.sock));
|
||||
Combat::genQItemRolls(playerRefs, qitemRolls);
|
||||
for (int i = 0; i < players.size(); i++) {
|
||||
CNSocket* sockTo = players[i].sock;
|
||||
Player* otherPlr = PlayerManager::getPlayer(sockTo);
|
||||
|
||||
// only contribute to group members' kills if they're close enough
|
||||
int dist = std::hypot(plr->x - otherPlr->x + 1, plr->y - otherPlr->y + 1);
|
||||
if (dist > 5000)
|
||||
continue;
|
||||
|
||||
Items::giveMobDrop(sockTo, self, rolled, eventRolled);
|
||||
Missions::mobKilled(sockTo, self->type, qitemRolls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delay the despawn animation
|
||||
self->despawned = false;
|
||||
|
||||
auto it = Transport::NPCQueues.find(self->id);
|
||||
if (it == Transport::NPCQueues.end() || it->second.empty())
|
||||
return;
|
||||
|
||||
// rewind or empty the movement queue
|
||||
if (self->staticPath) {
|
||||
/*
|
||||
* This is inelegant, but we wind forward in the path until we find the point that
|
||||
* corresponds with the Mob's spawn point.
|
||||
*
|
||||
* IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever.
|
||||
*/
|
||||
auto& queue = it->second;
|
||||
for (auto point = queue.front(); point.x != self->spawnX || point.y != self->spawnY; point = queue.front()) {
|
||||
queue.pop();
|
||||
queue.push(point);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Transport::NPCQueues.erase(self->id);
|
||||
}
|
||||
}
|
||||
114
src/MobAI.hpp
Normal file
114
src/MobAI.hpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "JSON.hpp"
|
||||
|
||||
#include "Entities.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
namespace MobAI {
|
||||
void deadStep(CombatNPC* self, time_t currTime);
|
||||
void combatStep(CombatNPC* self, time_t currTime);
|
||||
void roamingStep(CombatNPC* self, time_t currTime);
|
||||
void retreatStep(CombatNPC* self, time_t currTime);
|
||||
|
||||
void onRoamStart(CombatNPC* self, EntityRef src);
|
||||
void onCombatStart(CombatNPC* self, EntityRef src);
|
||||
void onRetreat(CombatNPC* self, EntityRef src);
|
||||
void onDeath(CombatNPC* self, EntityRef src);
|
||||
}
|
||||
|
||||
struct Mob : public CombatNPC {
|
||||
|
||||
// dead
|
||||
time_t killedTime = 0;
|
||||
time_t regenTime = 0;
|
||||
bool summoned = false;
|
||||
bool despawned = false; // for the sake of death animations
|
||||
|
||||
// roaming
|
||||
int idleRange = 0;
|
||||
const int sightRange = 0;
|
||||
time_t nextMovement = 0;
|
||||
bool staticPath = false;
|
||||
int roamX = 0, roamY = 0, roamZ = 0;
|
||||
|
||||
// combat
|
||||
CNSocket *target = nullptr;
|
||||
time_t nextAttack = 0;
|
||||
time_t lastDrainTime = 0;
|
||||
int skillStyle = -1; // -1 for nothing, 0-2 for corruption, -2 for eruption
|
||||
int hitX = 0, hitY = 0, hitZ = 0; // for use in ability targeting
|
||||
|
||||
// group
|
||||
int groupLeader = 0;
|
||||
int offsetX = 0, offsetY = 0;
|
||||
int groupMember[4] = {};
|
||||
|
||||
// temporary; until we're sure what's what
|
||||
nlohmann::json data = {};
|
||||
|
||||
Mob(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
|
||||
: CombatNPC(spawnX, spawnY, spawnZ, angle, iID, t, id, d["m_iHP"]),
|
||||
sightRange(d["m_iSightRange"]) {
|
||||
state = AIState::ROAMING;
|
||||
|
||||
data = d;
|
||||
|
||||
speed = data["m_iRunSpeed"];
|
||||
regenTime = data["m_iRegenTime"];
|
||||
idleRange = (int)data["m_iIdleRange"];
|
||||
level = data["m_iNpcLevel"];
|
||||
|
||||
roamX = spawnX;
|
||||
roamY = spawnY;
|
||||
roamZ = spawnZ;
|
||||
|
||||
offsetX = 0;
|
||||
offsetY = 0;
|
||||
|
||||
// NOTE: there appear to be discrepancies in the dump
|
||||
hp = maxHealth;
|
||||
|
||||
kind = EntityKind::MOB;
|
||||
|
||||
// AI
|
||||
stateHandlers[AIState::DEAD] = MobAI::deadStep;
|
||||
stateHandlers[AIState::COMBAT] = MobAI::combatStep;
|
||||
stateHandlers[AIState::ROAMING] = MobAI::roamingStep;
|
||||
stateHandlers[AIState::RETREAT] = MobAI::retreatStep;
|
||||
|
||||
transitionHandlers[AIState::DEAD] = MobAI::onDeath;
|
||||
transitionHandlers[AIState::COMBAT] = MobAI::onCombatStart;
|
||||
transitionHandlers[AIState::ROAMING] = MobAI::onRoamStart;
|
||||
transitionHandlers[AIState::RETREAT] = MobAI::onRetreat;
|
||||
}
|
||||
|
||||
// constructor for /summon
|
||||
Mob(int x, int y, int z, uint64_t iID, int t, nlohmann::json d, int32_t id)
|
||||
: Mob(x, y, z, 0, iID, t, d, id) {
|
||||
summoned = true; // will be despawned and deallocated when killed
|
||||
}
|
||||
|
||||
~Mob() {}
|
||||
|
||||
virtual int takeDamage(EntityRef src, int amt) override;
|
||||
virtual void step(time_t currTime) override;
|
||||
|
||||
auto operator[](std::string s) {
|
||||
return data[s];
|
||||
}
|
||||
};
|
||||
|
||||
namespace MobAI {
|
||||
extern bool simulateMobs;
|
||||
|
||||
// TODO: make this internal later
|
||||
void incNextMovement(Mob *mob, time_t currTime=0);
|
||||
bool aggroCheck(Mob *mob, time_t currTime);
|
||||
void clearDebuff(Mob *mob);
|
||||
void followToCombat(Mob *mob);
|
||||
void groupRetreat(Mob *mob);
|
||||
}
|
||||
37
src/NPC.hpp
37
src/NPC.hpp
@@ -1,37 +0,0 @@
|
||||
#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();
|
||||
}
|
||||
};
|
||||
@@ -1,193 +1,406 @@
|
||||
#include "NPCManager.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "Combat.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Chunking.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "TableData.hpp"
|
||||
#include "Groups.hpp"
|
||||
#include "Racing.hpp"
|
||||
#include "Vendors.hpp"
|
||||
#include "Abilities.hpp"
|
||||
#include "Rand.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <list>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <assert.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include "contrib/JSON.hpp"
|
||||
using namespace NPCManager;
|
||||
|
||||
std::map<int32_t, BaseNPC> NPCManager::NPCs;
|
||||
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->angle = angle;
|
||||
ChunkPos oldChunk = npc->chunkPos;
|
||||
ChunkPos newChunk = Chunking::chunkPosAt(X, Y, I);
|
||||
npc->x = X;
|
||||
npc->y = Y;
|
||||
npc->z = Z;
|
||||
npc->instanceID = I;
|
||||
if (oldChunk == newChunk)
|
||||
return; // didn't change chunks
|
||||
Chunking::updateEntityChunk({id}, oldChunk, newChunk);
|
||||
}
|
||||
|
||||
void NPCManager::sendToViewable(Entity *npc, void *buf, uint32_t type, size_t size) {
|
||||
for (auto it = npc->viewableChunks.begin(); it != npc->viewableChunks.end(); it++) {
|
||||
Chunk* chunk = *it;
|
||||
for (const EntityRef& ref : chunk->entities) {
|
||||
if (ref.kind == EntityKind::PLAYER)
|
||||
ref.sock->sendPacket(buf, type, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void npcBarkHandler(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf;
|
||||
|
||||
int taskID = req->iMissionTaskID;
|
||||
// ignore req->iNPC_ID as it is often fixated on a single npc in the region
|
||||
|
||||
if (Missions::Tasks.find(taskID) == Missions::Tasks.end()) {
|
||||
std::cout << "mission task not found: " << taskID << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
TaskData* td = Missions::Tasks[taskID];
|
||||
auto& barks = td->task["m_iHBarkerTextID"];
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
std::vector<std::pair<int32_t, int32_t>> npcLines;
|
||||
|
||||
for (Chunk* chunk : plr->viewableChunks) {
|
||||
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
|
||||
if (ent->kind != EntityKind::SIMPLE_NPC)
|
||||
continue;
|
||||
|
||||
BaseNPC* npc = (BaseNPC*)ent->getEntity();
|
||||
if (npc->type < 0 || npc->type >= NPCData.size())
|
||||
continue; // npc unknown ?!
|
||||
|
||||
int barkType = NPCData[npc->type]["m_iBarkerType"];
|
||||
if (barkType < 1 || barkType > 4)
|
||||
continue; // no barks
|
||||
|
||||
int barkID = barks[barkType - 1];
|
||||
if (barkID == 0)
|
||||
continue; // no barks
|
||||
|
||||
npcLines.push_back(std::make_pair(npc->id, barkID));
|
||||
}
|
||||
}
|
||||
|
||||
if (npcLines.size() == 0)
|
||||
return; // totally no barks
|
||||
|
||||
auto& [npcID, missionStringID] = npcLines[Rand::rand(npcLines.size())];
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_BARKER, resp);
|
||||
resp.iNPC_ID = npcID;
|
||||
resp.iMissionStringID = missionStringID;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_BARKER);
|
||||
}
|
||||
|
||||
static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (plr->accountLevel > 30)
|
||||
return;
|
||||
|
||||
// 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 spawnX, int spawnY, int spawnZ, uint64_t instance, int type, bool respawn, bool baseInstance) {
|
||||
uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
|
||||
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC, sizeof(sP_FE2CL_REP_PC_WARP_USE_NPC_SUCC));
|
||||
}
|
||||
int id = nextId--;
|
||||
int team = NPCData[type]["m_iTeam"];
|
||||
BaseNPC *npc = nullptr;
|
||||
|
||||
if (team == 2) {
|
||||
npc = new Mob(spawnX, spawnY, spawnZ, inst, type, NPCData[type], id);
|
||||
|
||||
// re-enable respawning, if desired
|
||||
((Mob*)npc)->summoned = !respawn;
|
||||
} else
|
||||
npc = new BaseNPC(0, inst, type, id);
|
||||
|
||||
NPCs[id] = npc;
|
||||
|
||||
return npc;
|
||||
}
|
||||
|
||||
static void npcSummonHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_NPC_SUMMON*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int limit = NPCData.back()["m_iNpcNumber"];
|
||||
|
||||
// permission & sanity check
|
||||
if (plr->accountLevel > 30 || req->iNPCType > limit || req->iNPCCnt > 100)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < req->iNPCCnt; i++) {
|
||||
BaseNPC *npc = summonNPC(plr->x, plr->y, plr->z, plr->instanceID, req->iNPCType);
|
||||
updateNPCPosition(npc->id, plr->x, plr->y, plr->z, plr->instanceID, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void handleWarp(CNSocket* sock, int32_t warpId) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
// sanity check
|
||||
if (Warps.find(warpId) == Warps.end())
|
||||
return;
|
||||
|
||||
if (plr->iPCState & 8) {
|
||||
// remove the player's vehicle
|
||||
plr->iPCState &= ~8;
|
||||
|
||||
// send to self
|
||||
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, off);
|
||||
sock->sendPacket(off, P_FE2CL_PC_VEHICLE_OFF_SUCC);
|
||||
|
||||
// send to others
|
||||
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, chg);
|
||||
chg.iPC_ID = plr->iID;
|
||||
chg.iState = plr->iPCState;
|
||||
PlayerManager::sendToViewable(sock, chg, P_FE2CL_PC_STATE_CHANGE);
|
||||
}
|
||||
|
||||
// std::cerr << "Warped to Map Num:" << Warps[warpId].instanceID << " NPC ID " << Warps[warpId].npcID << std::endl;
|
||||
if (Warps[warpId].isInstance) {
|
||||
uint64_t instanceID = Warps[warpId].instanceID;
|
||||
|
||||
Player* leader = plr;
|
||||
if (plr->group != nullptr) leader = PlayerManager::getPlayer(plr->group->filter(EntityKind::PLAYER)[0].sock);
|
||||
|
||||
// if warp requires you to be on a mission, it's gotta be a unique instance
|
||||
if (Warps[warpId].limitTaskID != 0 || instanceID == 14) { // 14 is a special case for the Time Lab
|
||||
instanceID += ((uint64_t)leader->iID << 32); // upper 32 bits are leader ID
|
||||
Chunking::createInstance(instanceID);
|
||||
|
||||
// save Lair entrance coords as a pseudo-Resurrect 'Em
|
||||
plr->recallX = Warps[warpId].x;
|
||||
plr->recallY = Warps[warpId].y;
|
||||
plr->recallZ = Warps[warpId].z + RESURRECT_HEIGHT;
|
||||
plr->recallInstance = instanceID;
|
||||
}
|
||||
|
||||
if (plr->group == nullptr)
|
||||
PlayerManager::sendPlayerTo(sock, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
|
||||
else {
|
||||
auto players = plr->group->filter(EntityKind::PLAYER);
|
||||
for (int i = 0; i < players.size(); i++) {
|
||||
CNSocket* sockTo = players[i].sock;
|
||||
Player* otherPlr = PlayerManager::getPlayer(sockTo);
|
||||
|
||||
if (otherPlr == nullptr || sockTo == nullptr)
|
||||
continue;
|
||||
|
||||
// save Lair entrance coords for everyone else as well
|
||||
otherPlr->recallX = Warps[warpId].x;
|
||||
otherPlr->recallY = Warps[warpId].y;
|
||||
otherPlr->recallZ = Warps[warpId].z + RESURRECT_HEIGHT;
|
||||
otherPlr->recallInstance = instanceID;
|
||||
|
||||
// remove their vehicle if they're on one
|
||||
if (otherPlr->iPCState & 8) {
|
||||
// remove the player's vehicle
|
||||
otherPlr->iPCState &= ~8;
|
||||
|
||||
// send to self
|
||||
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, off);
|
||||
sockTo->sendPacket(off, P_FE2CL_PC_VEHICLE_OFF_SUCC);
|
||||
|
||||
// send to others
|
||||
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, chg);
|
||||
chg.iPC_ID = otherPlr->iID;
|
||||
chg.iState = otherPlr->iPCState;
|
||||
PlayerManager::sendToViewable(sockTo, chg, P_FE2CL_PC_STATE_CHANGE);
|
||||
}
|
||||
|
||||
PlayerManager::sendPlayerTo(sockTo, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_NPC_SUCC, resp); // Can only be used for exiting instances because it sets the instance flag to false
|
||||
resp.iX = Warps[warpId].x;
|
||||
resp.iY = Warps[warpId].y;
|
||||
resp.iZ = Warps[warpId].z;
|
||||
resp.iCandy = plr->money;
|
||||
resp.eIL = 4; // do not take away any items
|
||||
uint64_t fromInstance = plr->instanceID; // pre-warp instance, saved for post-warp
|
||||
plr->instanceID = INSTANCE_OVERWORLD;
|
||||
Missions::failInstancedMissions(sock); // fail any instanced missions
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC);
|
||||
|
||||
PlayerManager::updatePlayerPositionForWarp(sock, resp.iX, resp.iY, resp.iZ, INSTANCE_OVERWORLD);
|
||||
|
||||
// remove the player's ongoing race, if any
|
||||
if (Racing::EPRaces.find(sock) != Racing::EPRaces.end())
|
||||
Racing::EPRaces.erase(sock);
|
||||
|
||||
// post-warp: check if the source instance has no more players in it and delete it if so
|
||||
Chunking::destroyInstanceIfEmpty(fromInstance);
|
||||
}
|
||||
}
|
||||
|
||||
static void npcWarpHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto warpNpc = (sP_CL2FE_REQ_PC_WARP_USE_NPC*)data->buf;
|
||||
handleWarp(sock, warpNpc->iWarpID);
|
||||
}
|
||||
|
||||
static void npcWarpTimeMachine(CNSocket* sock, CNPacketData* data) {
|
||||
// this is just a warp request
|
||||
handleWarp(sock, 28);
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function to get NPC closest to coordinates in specified chunks
|
||||
*/
|
||||
BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z) {
|
||||
BaseNPC* npc = nullptr;
|
||||
int lastDist = INT_MAX;
|
||||
for (auto c = chunks->begin(); c != chunks->end(); c++) { // haha get it
|
||||
Chunk* chunk = *c;
|
||||
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
|
||||
if (ent->kind == EntityKind::PLAYER)
|
||||
continue;
|
||||
|
||||
BaseNPC* npcTemp = (BaseNPC*)ent->getEntity();
|
||||
int distXY = std::hypot(X - npcTemp->x, Y - npcTemp->y);
|
||||
int dist = std::hypot(distXY, Z - npcTemp->z);
|
||||
if (dist < lastDist) {
|
||||
npc = npcTemp;
|
||||
lastDist = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
return npc;
|
||||
}
|
||||
|
||||
// TODO: Move this to separate file in ai/ subdir when implementing more events
|
||||
#pragma region NPCEvents
|
||||
|
||||
// summon right arm and stage 2 body
|
||||
static void lordFuseStageTwo(CombatNPC *npc) {
|
||||
Mob *oldbody = (Mob*)npc; // adaptium, stun
|
||||
|
||||
std::cout << "Lord Fuse stage two" << std::endl;
|
||||
|
||||
// Fuse doesn't move
|
||||
// Blastons, Heal
|
||||
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2467);
|
||||
|
||||
newbody->angle = oldbody->angle;
|
||||
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
|
||||
oldbody->instanceID, oldbody->angle);
|
||||
|
||||
// right arm, Adaptium, Stun
|
||||
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, oldbody->instanceID, 2469);
|
||||
|
||||
arm->angle = oldbody->angle;
|
||||
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
|
||||
oldbody->instanceID, oldbody->angle);
|
||||
}
|
||||
|
||||
// summon left arm and stage 3 body
|
||||
static void lordFuseStageThree(CombatNPC *npc) {
|
||||
Mob *oldbody = (Mob*)npc;
|
||||
|
||||
std::cout << "Lord Fuse stage three" << std::endl;
|
||||
|
||||
// Cosmix, Damage Point
|
||||
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2468);
|
||||
|
||||
newbody->angle = oldbody->angle;
|
||||
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
|
||||
newbody->instanceID, oldbody->angle);
|
||||
|
||||
// Blastons, Heal
|
||||
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, oldbody->instanceID, 2470);
|
||||
|
||||
arm->angle = oldbody->angle;
|
||||
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
|
||||
arm->instanceID, oldbody->angle);
|
||||
}
|
||||
|
||||
std::vector<NPCEvent> NPCManager::NPCEvents = {
|
||||
NPCEvent(2466, AIState::DEAD, lordFuseStageTwo),
|
||||
NPCEvent(2467, AIState::DEAD, lordFuseStageThree),
|
||||
};
|
||||
|
||||
#pragma endregion NPCEvents
|
||||
|
||||
void NPCManager::queueNPCRemoval(int32_t id) {
|
||||
RemovalQueue.push(id);
|
||||
}
|
||||
|
||||
static void step(CNServer *serv, time_t currTime) {
|
||||
for (auto& pair : NPCs) {
|
||||
if (pair.second->kind != EntityKind::COMBAT_NPC && pair.second->kind != EntityKind::MOB)
|
||||
continue;
|
||||
auto npc = (CombatNPC*)pair.second;
|
||||
|
||||
npc->step(currTime);
|
||||
}
|
||||
|
||||
// deallocate all NPCs queued for removal
|
||||
while (RemovalQueue.size() > 0) {
|
||||
NPCManager::destroyNPC(RemovalQueue.front());
|
||||
RemovalQueue.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void NPCManager::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_USE_NPC, npcWarpHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TIME_TO_GO_WARP, npcWarpTimeMachine);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_SUMMON, npcSummonHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler);
|
||||
|
||||
REGISTER_SHARD_TIMER(step, MS_PER_COMBAT_TICK);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "CNProtocol.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "NPC.hpp"
|
||||
#include "core/Core.hpp"
|
||||
#include "JSON.hpp"
|
||||
|
||||
#include "Transport.hpp"
|
||||
#include "Chunking.hpp"
|
||||
#include "Entities.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
|
||||
#define RESURRECT_HEIGHT 400
|
||||
|
||||
// this should really be called vec3 or something...
|
||||
struct WarpLocation {
|
||||
int x, y, z;
|
||||
typedef void (*NPCEventHandler)(CombatNPC*);
|
||||
|
||||
struct NPCEvent {
|
||||
int32_t npcType;
|
||||
AIState triggerState;
|
||||
NPCEventHandler handler;
|
||||
|
||||
NPCEvent(int32_t t, AIState tr, NPCEventHandler hndlr)
|
||||
: npcType(t), triggerState(tr), handler(hndlr) {}
|
||||
};
|
||||
|
||||
namespace NPCManager {
|
||||
extern std::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(Entity* npc, void* buf, uint32_t type, size_t size);
|
||||
|
||||
BaseNPC *summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn=false, bool baseInstance=false);
|
||||
|
||||
BaseNPC* getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
373
src/Nanos.cpp
Normal file
373
src/Nanos.cpp
Normal file
@@ -0,0 +1,373 @@
|
||||
#include "Nanos.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Abilities.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using namespace Nanos;
|
||||
|
||||
std::map<int32_t, NanoData> Nanos::NanoTable;
|
||||
std::map<int32_t, NanoTuning> Nanos::NanoTunings;
|
||||
|
||||
#pragma region Helper methods
|
||||
void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm) {
|
||||
if (nanoID <= 0 || nanoID >= NANO_COUNT)
|
||||
return;
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int level = plr->level;
|
||||
|
||||
#ifndef ACADEMY
|
||||
level = nanoID < plr->level ? plr->level : nanoID;
|
||||
|
||||
/*
|
||||
* Spend the necessary Fusion Matter.
|
||||
* Note the use of the not-yet-incremented plr->level as opposed to level.
|
||||
* Doing it the other way always leaves the FM at 0. Jade totally called it.
|
||||
*/
|
||||
plr->level = level;
|
||||
|
||||
if (spendfm)
|
||||
Missions::updateFusionMatter(sock, -(int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]);
|
||||
#endif
|
||||
|
||||
// Send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_NANO_CREATE_SUCC, resp);
|
||||
resp.Nano.iID = nanoID;
|
||||
resp.Nano.iStamina = 150;
|
||||
resp.iQuestItemSlotNum = slot;
|
||||
resp.iPC_Level = level;
|
||||
resp.iPC_FusionMatter = plr->fusionmatter;
|
||||
|
||||
if (plr->activeNano > 0 && plr->activeNano == nanoID)
|
||||
summonNano(sock, -1); // just unsummon the nano to prevent infinite buffs
|
||||
|
||||
// Update player
|
||||
plr->Nanos[nanoID] = resp.Nano;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_NANO_CREATE_SUCC);
|
||||
|
||||
/*
|
||||
* iPC_Level in NANO_CREATE_SUCC sets the player's level.
|
||||
* Other players must be notified of the change as well. Both P_FE2CL_REP_PC_NANO_CREATE and
|
||||
* P_FE2CL_REP_PC_CHANGE_LEVEL appear to play the same animation, but only the latter affects
|
||||
* the other player's displayed level.
|
||||
*/
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL, resp2);
|
||||
|
||||
resp2.iPC_ID = plr->iID;
|
||||
resp2.iPC_Level = level;
|
||||
|
||||
// Update other players' perception of the player's level
|
||||
PlayerManager::sendToViewable(sock, resp2, P_FE2CL_REP_PC_CHANGE_LEVEL);
|
||||
}
|
||||
|
||||
void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
|
||||
INITSTRUCT(sP_FE2CL_REP_NANO_ACTIVE_SUCC, resp);
|
||||
resp.iActiveNanoSlotNum = slot;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (slot > 2 || slot < -1)
|
||||
return; // sanity check
|
||||
|
||||
int16_t nanoID = slot == -1 ? 0 : plr->equippedNanos[slot];
|
||||
|
||||
if (slot != -1 && plr->Nanos[nanoID].iSkillID == 0)
|
||||
return; // prevent powerless nanos from summoning
|
||||
|
||||
if (nanoID >= NANO_COUNT || nanoID < 0)
|
||||
return; // sanity check
|
||||
|
||||
plr->activeNano = nanoID;
|
||||
sNano& nano = plr->Nanos[nanoID];
|
||||
|
||||
SkillData* skill = Abilities::SkillTable.count(nano.iSkillID) > 0
|
||||
? &Abilities::SkillTable[nano.iSkillID] : nullptr;
|
||||
if (slot != -1 && skill != nullptr && skill->drainType == SkillDrainType::PASSIVE) {
|
||||
// passive buff effect
|
||||
resp.eCSTB___Add = 1;
|
||||
ICombatant* src = dynamic_cast<ICombatant*>(plr);
|
||||
int32_t targets[] = { plr->iID };
|
||||
std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets);
|
||||
Abilities::useNanoSkill(sock, skill, nano, affectedCombatants);
|
||||
}
|
||||
|
||||
if (!silent) // silent nano death but only for the summoning player
|
||||
sock->sendPacket(resp, P_FE2CL_REP_NANO_ACTIVE_SUCC);
|
||||
|
||||
// Send to other players, these players can't handle silent nano deaths so this packet needs to be sent.
|
||||
INITSTRUCT(sP_FE2CL_NANO_ACTIVE, pkt1);
|
||||
pkt1.iPC_ID = plr->iID;
|
||||
pkt1.Nano = nano;
|
||||
PlayerManager::sendToViewable(sock, pkt1, P_FE2CL_NANO_ACTIVE);
|
||||
}
|
||||
|
||||
static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
|
||||
if (skill == nullptr || skill->iNanoID >= NANO_COUNT || skill->iNanoID < 0)
|
||||
return;
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (plr->activeNano > 0 && plr->activeNano == skill->iNanoID)
|
||||
summonNano(sock, -1); // just unsummon the nano to prevent infinite buffs
|
||||
|
||||
sNano nano = plr->Nanos[skill->iNanoID];
|
||||
nano.iSkillID = skill->iTuneID;
|
||||
plr->Nanos[skill->iNanoID] = nano;
|
||||
|
||||
// Send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_NANO_TUNE_SUCC, resp);
|
||||
resp.iNanoID = skill->iNanoID;
|
||||
resp.iSkillID = skill->iTuneID;
|
||||
resp.iPC_FusionMatter = plr->fusionmatter;
|
||||
resp.aItem[9] = plr->Inven[0]; // quick fix to make sure item in slot 0 doesn't get yeeted by default
|
||||
|
||||
|
||||
// check if there's any garbage in the item slot array (this'll happen when a nano station isn't used)
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (skill->aiNeedItemSlotNum[i] < 0 || skill->aiNeedItemSlotNum[i] >= AINVEN_COUNT) {
|
||||
sock->sendPacket(resp, P_FE2CL_REP_NANO_TUNE_SUCC);
|
||||
return; // stop execution, don't run consumption logic
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef ACADEMY
|
||||
if (plr->fusionmatter < (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"]) // sanity check
|
||||
return;
|
||||
#endif
|
||||
|
||||
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"];
|
||||
|
||||
int reqItemCount = NanoTunings[skill->iTuneID].reqItemCount;
|
||||
int reqItemID = NanoTunings[skill->iTuneID].reqItems;
|
||||
int i = 0;
|
||||
while (reqItemCount > 0 && i < 10) {
|
||||
|
||||
sItemBase& item = plr->Inven[skill->aiNeedItemSlotNum[i]];
|
||||
if (item.iType == 7 && item.iID == reqItemID) {
|
||||
if (item.iOpt > reqItemCount) {
|
||||
item.iOpt -= reqItemCount;
|
||||
reqItemCount = 0;
|
||||
}
|
||||
else {
|
||||
reqItemCount -= item.iOpt;
|
||||
item.iID = 0;
|
||||
item.iType = 0;
|
||||
item.iOpt = 0;
|
||||
}
|
||||
}
|
||||
i++; // next slot
|
||||
}
|
||||
|
||||
resp.iPC_FusionMatter = plr->fusionmatter; // update fusion matter in packet
|
||||
// update items clientside
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (skill->aiNeedItemSlotNum[i]) { // non-zero check
|
||||
resp.aItem[i] = plr->Inven[skill->aiNeedItemSlotNum[i]];
|
||||
resp.aiItemSlotNum[i] = skill->aiNeedItemSlotNum[i];
|
||||
}
|
||||
}
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_NANO_TUNE_SUCC);
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << PlayerManager::getPlayerName(plr) << " set skill id " << skill->iTuneID << " for nano: " << skill->iNanoID << std::endl;
|
||||
)
|
||||
}
|
||||
|
||||
// 0=A 1=B 2=C -1=Not found
|
||||
int Nanos::nanoStyle(int nanoID) {
|
||||
if (nanoID < 1 || nanoID >= (int)NanoTable.size())
|
||||
return -1;
|
||||
return NanoTable[nanoID].style;
|
||||
}
|
||||
|
||||
bool Nanos::getNanoBoost(Player* plr) {
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (plr->equippedNanos[i] == plr->activeNano)
|
||||
if (plr->hasBuff(ECSB_STIMPAKSLOT1 + i))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
static void nanoEquipHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto nano = (sP_CL2FE_REQ_NANO_EQUIP*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_NANO_EQUIP_SUCC, resp);
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// sanity checks
|
||||
if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0)
|
||||
return;
|
||||
|
||||
if (nano->iNanoID < 0 || nano->iNanoID >= NANO_COUNT)
|
||||
return;
|
||||
|
||||
resp.iNanoID = nano->iNanoID;
|
||||
resp.iNanoSlotNum = nano->iNanoSlotNum;
|
||||
|
||||
// Update player
|
||||
plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID;
|
||||
|
||||
// unsummon nano if replaced
|
||||
if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum])
|
||||
summonNano(sock, -1);
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_NANO_EQUIP_SUCC);
|
||||
}
|
||||
|
||||
static void nanoUnEquipHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto nano = (sP_CL2FE_REQ_NANO_UNEQUIP*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_NANO_UNEQUIP_SUCC, resp);
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// sanity check
|
||||
if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0)
|
||||
return;
|
||||
|
||||
resp.iNanoSlotNum = nano->iNanoSlotNum;
|
||||
|
||||
// unsummon nano if removed
|
||||
if (plr->equippedNanos[nano->iNanoSlotNum] == plr->activeNano)
|
||||
summonNano(sock, -1);
|
||||
|
||||
// update player
|
||||
plr->equippedNanos[nano->iNanoSlotNum] = 0;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_NANO_UNEQUIP_SUCC);
|
||||
}
|
||||
|
||||
static void nanoSummonHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_NANO_ACTIVE*)data->buf;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
summonNano(sock, pkt->iNanoSlotNum);
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano slot: " << pkt->iNanoSlotNum << std::endl;
|
||||
)
|
||||
}
|
||||
|
||||
static void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// validate request check
|
||||
sP_CL2FE_REQ_NANO_SKILL_USE* pkt = (sP_CL2FE_REQ_NANO_SKILL_USE*)data->buf;
|
||||
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_NANO_SKILL_USE), pkt->iTargetCnt, sizeof(int32_t), data->size)) {
|
||||
std::cout << "[WARN] bad sP_CL2FE_REQ_NANO_SKILL_USE packet size" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
sNano& nano = plr->Nanos[plr->activeNano];
|
||||
int16_t skillID = nano.iSkillID;
|
||||
SkillData* skillData = &Abilities::SkillTable[skillID];
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano skill " << std::endl;
|
||||
)
|
||||
|
||||
ICombatant* plrCombatant = dynamic_cast<ICombatant*>(plr);
|
||||
std::vector<ICombatant*> targetData = Abilities::matchTargets(plrCombatant, skillData, pkt->iTargetCnt, (int32_t*)(pkt + 1));
|
||||
Abilities::useNanoSkill(sock, skillData, nano, targetData);
|
||||
|
||||
if (plr->Nanos[plr->activeNano].iStamina <= 0)
|
||||
summonNano(sock, -1);
|
||||
}
|
||||
|
||||
static void nanoSkillSetHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto skill = (sP_CL2FE_REQ_NANO_TUNE*)data->buf;
|
||||
setNanoSkill(sock, skill);
|
||||
}
|
||||
|
||||
static void nanoSkillSetGMHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto skillGM = (sP_CL2FE_REQ_NANO_TUNE*)data->buf;
|
||||
setNanoSkill(sock, skillGM);
|
||||
}
|
||||
|
||||
static void nanoRecallRegisterHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto recallData = (sP_CL2FE_REQ_REGIST_RXCOM*)data->buf;
|
||||
|
||||
if (NPCManager::NPCs.find(recallData->iNPCID) == NPCManager::NPCs.end())
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
BaseNPC *npc = NPCManager::NPCs[recallData->iNPCID];
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_REGIST_RXCOM, response);
|
||||
response.iMapNum = plr->recallInstance = (int32_t)npc->instanceID; // Never going to recall into a Fusion Lair
|
||||
response.iX = plr->recallX = npc->x;
|
||||
response.iY = plr->recallY = npc->y;
|
||||
response.iZ = plr->recallZ = npc->z;
|
||||
sock->sendPacket(response, P_FE2CL_REP_REGIST_RXCOM);
|
||||
}
|
||||
|
||||
static void nanoRecallHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto recallData = (sP_CL2FE_REQ_WARP_USE_RECALL*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(recallData->iGroupMemberID);
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
// ensure the group member is still in the same IZ
|
||||
if (otherPlr->instanceID != plr->instanceID)
|
||||
return;
|
||||
|
||||
// do not allow hypothetical recall points in lairs to mess with the respawn logic
|
||||
if (PLAYERID(plr->instanceID) != 0)
|
||||
return;
|
||||
|
||||
if ((int32_t)plr->instanceID == otherPlr->recallInstance)
|
||||
PlayerManager::sendPlayerTo(sock, otherPlr->recallX, otherPlr->recallY, otherPlr->recallZ, otherPlr->recallInstance);
|
||||
else {
|
||||
INITSTRUCT(sP_FE2CL_REP_WARP_USE_RECALL_FAIL, response)
|
||||
sock->sendPacket(response, P_FE2CL_REP_WARP_USE_RECALL_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
static void nanoPotionHandler(CNSocket* sock, CNPacketData* data) {
|
||||
Player* player = PlayerManager::getPlayer(sock);
|
||||
|
||||
// sanity checks
|
||||
if (player->activeNano == -1 || player->batteryN == 0)
|
||||
return;
|
||||
|
||||
sNano nano = player->Nanos[player->activeNano];
|
||||
int difference = 150 - nano.iStamina;
|
||||
if (player->batteryN < difference)
|
||||
difference = player->batteryN;
|
||||
|
||||
if (difference == 0)
|
||||
return;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_CHARGE_NANO_STAMINA, response);
|
||||
response.iNanoID = nano.iID;
|
||||
response.iNanoStamina = nano.iStamina + difference;
|
||||
response.iBatteryN = player->batteryN - difference;
|
||||
|
||||
sock->sendPacket(response, P_FE2CL_REP_CHARGE_NANO_STAMINA);
|
||||
// now update serverside
|
||||
player->batteryN -= difference;
|
||||
player->Nanos[nano.iID].iStamina += difference;
|
||||
|
||||
}
|
||||
|
||||
void Nanos::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_ACTIVE, nanoSummonHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_EQUIP, nanoEquipHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_UNEQUIP, nanoUnEquipHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_TUNE, nanoSkillSetHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_NANO_SKILL, nanoSkillSetGMHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_SKILL_USE, nanoSkillUseHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REGIST_RXCOM, nanoRecallRegisterHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_WARP_USE_RECALL, nanoRecallHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_CHARGE_NANO_STAMINA, nanoPotionHandler);
|
||||
}
|
||||
29
src/Nanos.hpp
Normal file
29
src/Nanos.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "Abilities.hpp"
|
||||
|
||||
#include <map>
|
||||
|
||||
struct NanoData {
|
||||
int style;
|
||||
};
|
||||
|
||||
struct NanoTuning {
|
||||
int reqItemCount;
|
||||
int reqItems;
|
||||
};
|
||||
|
||||
namespace Nanos {
|
||||
extern std::map<int32_t, NanoData> NanoTable;
|
||||
extern std::map<int32_t, NanoTuning> NanoTunings;
|
||||
void init();
|
||||
|
||||
// Helper methods
|
||||
void addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm=false);
|
||||
void summonNano(CNSocket* sock, int slot, bool silent = false);
|
||||
int nanoStyle(int nanoID);
|
||||
bool getNanoBoost(Player* plr);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
#include "Player.hpp"
|
||||
135
src/Player.hpp
135
src/Player.hpp
@@ -1,35 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include "CNProtocol.hpp"
|
||||
#include "CNStructs.hpp"
|
||||
#include "Entities.hpp"
|
||||
#include "Groups.hpp"
|
||||
|
||||
struct Player {
|
||||
int accountId;
|
||||
int64_t SerialKey;
|
||||
int32_t iID;
|
||||
uint64_t FEKey;
|
||||
#include <vector>
|
||||
|
||||
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;
|
||||
/* forward declaration(s) */
|
||||
class Buff;
|
||||
struct BuffStack;
|
||||
|
||||
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;
|
||||
#define ACTIVE_MISSION_COUNT 6
|
||||
|
||||
#define PC_MAXHEALTH(level) (925 + 75 * (level))
|
||||
|
||||
struct Player : public Entity, public ICombatant {
|
||||
int accountId = 0;
|
||||
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
|
||||
int32_t iID = 0;
|
||||
|
||||
int level = 0;
|
||||
int HP = 0;
|
||||
int slot = 0; // player slot, not nano slot
|
||||
int16_t mentor = 0;
|
||||
int32_t money = 0;
|
||||
int32_t fusionmatter = 0;
|
||||
int32_t batteryW = 0;
|
||||
int32_t batteryN = 0;
|
||||
sPCStyle PCStyle = {};
|
||||
sPCStyle2 PCStyle2 = {};
|
||||
sNano Nanos[NANO_COUNT] = {}; // acquired nanos
|
||||
int equippedNanos[3] = {};
|
||||
int activeNano = 0; // active nano (index into Nanos)
|
||||
int8_t iPCState = 0;
|
||||
int32_t iWarpLocationFlag = 0;
|
||||
int64_t aSkywayLocationFlag[2] = {};
|
||||
int8_t iSpecialState = 0;
|
||||
std::unordered_map<int, Buff*> buffs = {};
|
||||
|
||||
int angle = 0;
|
||||
int lastX = 0, lastY = 0, lastZ = 0, lastAngle = 0;
|
||||
int recallX = 0, recallY = 0, recallZ = 0, recallInstance = 0; // also Lair entrances
|
||||
sItemBase Equip[AEQUIP_COUNT] = {};
|
||||
sItemBase Inven[AINVEN_COUNT] = {};
|
||||
sItemBase Bank[ABANK_COUNT] = {};
|
||||
sItemTrade Trade[12] = {};
|
||||
int32_t moneyInTrade = 0;
|
||||
bool isTrading = false;
|
||||
bool isTradeConfirm = false;
|
||||
|
||||
bool inCombat = false;
|
||||
bool onMonkey = false;
|
||||
int healCooldown = 0;
|
||||
|
||||
int pointDamage = 0;
|
||||
int groupDamage = 0;
|
||||
int fireRate = 0;
|
||||
int defense = 0;
|
||||
|
||||
int64_t aQuestFlag[16] = {};
|
||||
int tasks[ACTIVE_MISSION_COUNT] = {};
|
||||
int RemainingNPCCount[ACTIVE_MISSION_COUNT][3] = {};
|
||||
sItemBase QInven[AQINVEN_COUNT] = {};
|
||||
int32_t CurrentMissionID = 0;
|
||||
|
||||
sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {};
|
||||
|
||||
Group* group = nullptr;
|
||||
|
||||
bool notify = false;
|
||||
bool hidden = false;
|
||||
bool unwarpable = false;
|
||||
bool initialLoadDone = false;
|
||||
|
||||
int64_t buddyIDs[50] = {};
|
||||
bool isBuddyBlocked[50] = {};
|
||||
|
||||
uint64_t iFirstUseFlag[2] = {};
|
||||
time_t lastHeartbeat = 0;
|
||||
|
||||
int suspicionRating = 0;
|
||||
time_t lastShot = 0;
|
||||
std::vector<sItemBase> buyback = {};
|
||||
|
||||
Player() { kind = EntityKind::PLAYER; }
|
||||
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
|
||||
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
|
||||
virtual Buff* getBuff(int buffId) override;
|
||||
virtual void removeBuff(int buffId) override;
|
||||
virtual void removeBuff(int buffId, BuffClass buffClass) override;
|
||||
virtual void clearBuffs(bool force) override;
|
||||
virtual bool hasBuff(int buffId) override;
|
||||
virtual int getCompositeCondition() override;
|
||||
virtual int takeDamage(EntityRef src, int amt) override;
|
||||
virtual int heal(EntityRef src, int amt) override;
|
||||
virtual bool isAlive() override;
|
||||
virtual int getCurrentHP() override;
|
||||
virtual int getMaxHP() override;
|
||||
virtual int getLevel() override;
|
||||
virtual std::vector<EntityRef> getGroupMembers() override;
|
||||
virtual int32_t getCharType() override;
|
||||
virtual int32_t getID() override;
|
||||
virtual EntityRef getRef() override;
|
||||
|
||||
virtual void step(time_t currTime) override;
|
||||
|
||||
sNano* getActiveNano();
|
||||
sPCAppearanceData getAppearanceData();
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,58 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "CNProtocol.hpp"
|
||||
#include "CNStructs.hpp"
|
||||
#include "CNShardServer.hpp"
|
||||
#include "Chunking.hpp"
|
||||
#include "Transport.hpp"
|
||||
#include "Entities.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <list>
|
||||
|
||||
struct 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 sendToGroup(CNSocket *sock, void* buf, uint32_t type, size_t size);
|
||||
void sendToViewable(CNSocket *sock, void* buf, uint32_t type, size_t size);
|
||||
|
||||
// TODO: unify this under the new Entity system
|
||||
template<class T>
|
||||
void sendToViewable(CNSocket *sock, T& pkt, uint32_t type) {
|
||||
Player* plr = getPlayer(sock);
|
||||
for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) {
|
||||
Chunk* chunk = *it;
|
||||
for (const EntityRef& ref : chunk->entities) {
|
||||
if (ref.kind != EntityKind::PLAYER || ref.sock == sock)
|
||||
continue;
|
||||
|
||||
ref.sock->sendPacket(pkt, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
290
src/PlayerMovement.cpp
Normal file
290
src/PlayerMovement.cpp
Normal file
@@ -0,0 +1,290 @@
|
||||
#include "PlayerMovement.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "PlayerManager.hpp"
|
||||
#include "TableData.hpp"
|
||||
#include "core/Core.hpp"
|
||||
|
||||
static void movePlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto* moveData = (sP_CL2FE_REQ_PC_MOVE*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, moveData->iX, moveData->iY, moveData->iZ, plr->instanceID, moveData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_MOVE, moveResponse);
|
||||
|
||||
moveResponse.iID = plr->iID;
|
||||
moveResponse.cKeyValue = moveData->cKeyValue;
|
||||
|
||||
moveResponse.iX = moveData->iX;
|
||||
moveResponse.iY = moveData->iY;
|
||||
moveResponse.iZ = moveData->iZ;
|
||||
moveResponse.iAngle = moveData->iAngle;
|
||||
moveResponse.fVX = moveData->fVX;
|
||||
moveResponse.fVY = moveData->fVY;
|
||||
moveResponse.fVZ = moveData->fVZ;
|
||||
|
||||
moveResponse.iSpeed = moveData->iSpeed;
|
||||
moveResponse.iCliTime = moveData->iCliTime; // maybe don't send this??? seems unneeded...
|
||||
moveResponse.iSvrTime = tm;
|
||||
|
||||
PlayerManager::sendToViewable(sock, moveResponse, P_FE2CL_PC_MOVE);
|
||||
|
||||
// [gruntwork] check if player has a follower and move it
|
||||
if (TableData::RunningNPCPaths.find(plr->iID) != TableData::RunningNPCPaths.end()) {
|
||||
BaseNPC* follower = TableData::RunningNPCPaths[plr->iID].first;
|
||||
Transport::NPCQueues.erase(follower->id); // erase existing points
|
||||
std::queue<Vec3> queue;
|
||||
Vec3 from = { follower->x, follower->y, follower->z };
|
||||
float drag = 0.95f; // this ensures that they don't bump into the player
|
||||
Vec3 to = {
|
||||
(int)(follower->x + (moveData->iX - follower->x) * drag),
|
||||
(int)(follower->y + (moveData->iY - follower->y) * drag),
|
||||
(int)(follower->z + (moveData->iZ - follower->z) * drag)
|
||||
};
|
||||
|
||||
// add a route to the queue; to be processed in Transport::stepNPCPathing()
|
||||
Transport::lerp(&queue, from, to, NPC_DEFAULT_SPEED * 1.5); // little faster than typical
|
||||
Transport::NPCQueues[follower->id] = queue;
|
||||
}
|
||||
}
|
||||
|
||||
static void stopPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto stopData = (sP_CL2FE_REQ_PC_STOP*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, stopData->iX, stopData->iY, stopData->iZ, plr->instanceID, plr->angle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_STOP, stopResponse);
|
||||
|
||||
stopResponse.iID = plr->iID;
|
||||
|
||||
stopResponse.iX = stopData->iX;
|
||||
stopResponse.iY = stopData->iY;
|
||||
stopResponse.iZ = stopData->iZ;
|
||||
|
||||
stopResponse.iCliTime = stopData->iCliTime; // maybe don't send this??? seems unneeded...
|
||||
stopResponse.iSvrTime = tm;
|
||||
|
||||
PlayerManager::sendToViewable(sock, stopResponse, P_FE2CL_PC_STOP);
|
||||
}
|
||||
|
||||
static void jumpPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto jumpData = (sP_CL2FE_REQ_PC_JUMP*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, jumpData->iX, jumpData->iY, jumpData->iZ, plr->instanceID, jumpData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_JUMP, jumpResponse);
|
||||
|
||||
jumpResponse.iID = plr->iID;
|
||||
jumpResponse.cKeyValue = jumpData->cKeyValue;
|
||||
|
||||
jumpResponse.iX = jumpData->iX;
|
||||
jumpResponse.iY = jumpData->iY;
|
||||
jumpResponse.iZ = jumpData->iZ;
|
||||
jumpResponse.iAngle = jumpData->iAngle;
|
||||
jumpResponse.iVX = jumpData->iVX;
|
||||
jumpResponse.iVY = jumpData->iVY;
|
||||
jumpResponse.iVZ = jumpData->iVZ;
|
||||
|
||||
jumpResponse.iSpeed = jumpData->iSpeed;
|
||||
jumpResponse.iCliTime = jumpData->iCliTime; // maybe don't send this??? seems unneeded...
|
||||
jumpResponse.iSvrTime = tm;
|
||||
|
||||
PlayerManager::sendToViewable(sock, jumpResponse, P_FE2CL_PC_JUMP);
|
||||
}
|
||||
|
||||
static void jumppadPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto jumppadData = (sP_CL2FE_REQ_PC_JUMPPAD*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, jumppadData->iX, jumppadData->iY, jumppadData->iZ, plr->instanceID, jumppadData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_JUMPPAD, jumppadResponse);
|
||||
|
||||
jumppadResponse.iPC_ID = plr->iID;
|
||||
jumppadResponse.cKeyValue = jumppadData->cKeyValue;
|
||||
|
||||
jumppadResponse.iX = jumppadData->iX;
|
||||
jumppadResponse.iY = jumppadData->iY;
|
||||
jumppadResponse.iZ = jumppadData->iZ;
|
||||
jumppadResponse.iVX = jumppadData->iVX;
|
||||
jumppadResponse.iVY = jumppadData->iVY;
|
||||
jumppadResponse.iVZ = jumppadData->iVZ;
|
||||
|
||||
jumppadResponse.iCliTime = jumppadData->iCliTime;
|
||||
jumppadResponse.iSvrTime = tm;
|
||||
|
||||
PlayerManager::sendToViewable(sock, jumppadResponse, P_FE2CL_PC_JUMPPAD);
|
||||
}
|
||||
|
||||
static void launchPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto launchData = (sP_CL2FE_REQ_PC_LAUNCHER*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, launchData->iX, launchData->iY, launchData->iZ, plr->instanceID, launchData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_LAUNCHER, launchResponse);
|
||||
|
||||
launchResponse.iPC_ID = plr->iID;
|
||||
|
||||
launchResponse.iX = launchData->iX;
|
||||
launchResponse.iY = launchData->iY;
|
||||
launchResponse.iZ = launchData->iZ;
|
||||
launchResponse.iVX = launchData->iVX;
|
||||
launchResponse.iVY = launchData->iVY;
|
||||
launchResponse.iVZ = launchData->iVZ;
|
||||
launchResponse.iSpeed = launchData->iSpeed;
|
||||
launchResponse.iAngle = launchData->iAngle;
|
||||
|
||||
launchResponse.iCliTime = launchData->iCliTime;
|
||||
launchResponse.iSvrTime = tm;
|
||||
|
||||
PlayerManager::sendToViewable(sock, launchResponse, P_FE2CL_PC_LAUNCHER);
|
||||
}
|
||||
|
||||
static void ziplinePlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
sP_CL2FE_REQ_PC_ZIPLINE* ziplineData = (sP_CL2FE_REQ_PC_ZIPLINE*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, ziplineData->iX, ziplineData->iY, ziplineData->iZ, plr->instanceID, ziplineData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_ZIPLINE, ziplineResponse);
|
||||
|
||||
ziplineResponse.iPC_ID = plr->iID;
|
||||
ziplineResponse.iCliTime = ziplineData->iCliTime;
|
||||
ziplineResponse.iSvrTime = tm;
|
||||
ziplineResponse.iX = ziplineData->iX;
|
||||
ziplineResponse.iY = ziplineData->iY;
|
||||
ziplineResponse.iZ = ziplineData->iZ;
|
||||
ziplineResponse.fVX = ziplineData->fVX;
|
||||
ziplineResponse.fVY = ziplineData->fVY;
|
||||
ziplineResponse.fVZ = ziplineData->fVZ;
|
||||
ziplineResponse.fMovDistance = ziplineData->fMovDistance;
|
||||
ziplineResponse.fMaxDistance = ziplineData->fMaxDistance;
|
||||
ziplineResponse.fDummy = ziplineData->fDummy; // wtf is this for?
|
||||
ziplineResponse.iStX = ziplineData->iStX;
|
||||
ziplineResponse.iStY = ziplineData->iStY;
|
||||
ziplineResponse.iStZ = ziplineData->iStZ;
|
||||
ziplineResponse.bDown = ziplineData->bDown;
|
||||
ziplineResponse.iSpeed = ziplineData->iSpeed;
|
||||
ziplineResponse.iAngle = ziplineData->iAngle;
|
||||
ziplineResponse.iRollMax = ziplineData->iRollMax;
|
||||
ziplineResponse.iRoll = ziplineData->iRoll;
|
||||
|
||||
PlayerManager::sendToViewable(sock, ziplineResponse, P_FE2CL_PC_ZIPLINE);
|
||||
}
|
||||
|
||||
static void movePlatformPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto platformData = (sP_CL2FE_REQ_PC_MOVEPLATFORM*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, platformData->iX, platformData->iY, platformData->iZ, plr->instanceID, platformData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_MOVEPLATFORM, platResponse);
|
||||
|
||||
platResponse.iPC_ID = plr->iID;
|
||||
platResponse.iCliTime = platformData->iCliTime;
|
||||
platResponse.iSvrTime = tm;
|
||||
platResponse.iX = platformData->iX;
|
||||
platResponse.iY = platformData->iY;
|
||||
platResponse.iZ = platformData->iZ;
|
||||
platResponse.iAngle = platformData->iAngle;
|
||||
platResponse.fVX = platformData->fVX;
|
||||
platResponse.fVY = platformData->fVY;
|
||||
platResponse.fVZ = platformData->fVZ;
|
||||
platResponse.iLcX = platformData->iLcX;
|
||||
platResponse.iLcY = platformData->iLcY;
|
||||
platResponse.iLcZ = platformData->iLcZ;
|
||||
platResponse.iSpeed = platformData->iSpeed;
|
||||
platResponse.bDown = platformData->bDown;
|
||||
platResponse.cKeyValue = platformData->cKeyValue;
|
||||
platResponse.iPlatformID = platformData->iPlatformID;
|
||||
|
||||
PlayerManager::sendToViewable(sock, platResponse, P_FE2CL_PC_MOVEPLATFORM);
|
||||
}
|
||||
|
||||
static void moveSliderPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto sliderData = (sP_CL2FE_REQ_PC_MOVETRANSPORTATION*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, sliderData->iX, sliderData->iY, sliderData->iZ, plr->instanceID, sliderData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_MOVETRANSPORTATION, sliderResponse);
|
||||
|
||||
sliderResponse.iPC_ID = plr->iID;
|
||||
sliderResponse.iCliTime = sliderData->iCliTime;
|
||||
sliderResponse.iSvrTime = tm;
|
||||
sliderResponse.iX = sliderData->iX;
|
||||
sliderResponse.iY = sliderData->iY;
|
||||
sliderResponse.iZ = sliderData->iZ;
|
||||
sliderResponse.iAngle = sliderData->iAngle;
|
||||
sliderResponse.fVX = sliderData->fVX;
|
||||
sliderResponse.fVY = sliderData->fVY;
|
||||
sliderResponse.fVZ = sliderData->fVZ;
|
||||
sliderResponse.iLcX = sliderData->iLcX;
|
||||
sliderResponse.iLcY = sliderData->iLcY;
|
||||
sliderResponse.iLcZ = sliderData->iLcZ;
|
||||
sliderResponse.iSpeed = sliderData->iSpeed;
|
||||
sliderResponse.cKeyValue = sliderData->cKeyValue;
|
||||
sliderResponse.iT_ID = sliderData->iT_ID;
|
||||
|
||||
PlayerManager::sendToViewable(sock, sliderResponse, P_FE2CL_PC_MOVETRANSPORTATION);
|
||||
}
|
||||
|
||||
static void moveSlopePlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
sP_CL2FE_REQ_PC_SLOPE* slopeData = (sP_CL2FE_REQ_PC_SLOPE*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, slopeData->iX, slopeData->iY, slopeData->iZ, plr->instanceID, slopeData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_SLOPE, slopeResponse);
|
||||
|
||||
slopeResponse.iPC_ID = plr->iID;
|
||||
slopeResponse.iCliTime = slopeData->iCliTime;
|
||||
slopeResponse.iSvrTime = tm;
|
||||
slopeResponse.iX = slopeData->iX;
|
||||
slopeResponse.iY = slopeData->iY;
|
||||
slopeResponse.iZ = slopeData->iZ;
|
||||
slopeResponse.iAngle = slopeData->iAngle;
|
||||
slopeResponse.fVX = slopeData->fVX;
|
||||
slopeResponse.fVY = slopeData->fVY;
|
||||
slopeResponse.fVZ = slopeData->fVZ;
|
||||
slopeResponse.iSpeed = slopeData->iSpeed;
|
||||
slopeResponse.cKeyValue = slopeData->cKeyValue;
|
||||
slopeResponse.iSlopeID = slopeData->iSlopeID;
|
||||
|
||||
PlayerManager::sendToViewable(sock, slopeResponse, P_FE2CL_PC_SLOPE);
|
||||
}
|
||||
|
||||
void PlayerMovement::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_MOVE, movePlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_STOP, stopPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_JUMP, jumpPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_JUMPPAD, jumppadPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_LAUNCHER, launchPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ZIPLINE, ziplinePlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_MOVEPLATFORM, movePlatformPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_MOVETRANSPORTATION, moveSliderPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SLOPE, moveSlopePlayer);
|
||||
}
|
||||
5
src/PlayerMovement.hpp
Normal file
5
src/PlayerMovement.hpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace PlayerMovement {
|
||||
void init();
|
||||
};
|
||||
188
src/Racing.cpp
Normal file
188
src/Racing.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
#include "Racing.hpp"
|
||||
|
||||
#include "db/Database.hpp"
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "NPCManager.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Items.hpp"
|
||||
|
||||
using namespace Racing;
|
||||
|
||||
std::map<int32_t, EPInfo> Racing::EPData;
|
||||
std::map<CNSocket*, EPRace> Racing::EPRaces;
|
||||
std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> Racing::EPRewards;
|
||||
|
||||
static void racingStart(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_EP_RACE_START*)data->buf;
|
||||
|
||||
if (NPCManager::NPCs.find(req->iStartEcomID) == NPCManager::NPCs.end())
|
||||
return; // starting line agent not found
|
||||
|
||||
int mapNum = MAPNUM(NPCManager::NPCs[req->iStartEcomID]->instanceID);
|
||||
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
|
||||
return; // IZ not found
|
||||
|
||||
// make ongoing race entry
|
||||
EPRace race = { {}, req->iEPRaceMode, req->iEPTicketItemSlotNum, getTime() / 1000 };
|
||||
EPRaces[sock] = race;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_RACE_START_SUCC, resp);
|
||||
resp.iStartTick = 0; // ignored
|
||||
resp.iLimitTime = EPData[mapNum].maxTime;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_START_SUCC);
|
||||
}
|
||||
|
||||
static void racingGetPod(CNSocket* sock, CNPacketData* data) {
|
||||
if (EPRaces.find(sock) == EPRaces.end())
|
||||
return; // race not found
|
||||
|
||||
auto req = (sP_CL2FE_REQ_EP_GET_RING*)data->buf;
|
||||
|
||||
if (EPRaces[sock].collectedRings.count(req->iRingLID))
|
||||
return; // can't collect the same ring twice
|
||||
|
||||
// without an anticheat system, we really don't have a choice but to honor the request
|
||||
// TODO: proximity check so players can't cheat the race by replaying packets
|
||||
EPRaces[sock].collectedRings.insert(req->iRingLID);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_GET_RING_SUCC, resp);
|
||||
|
||||
resp.iRingLID = req->iRingLID;
|
||||
resp.iRingCount_Get = EPRaces[sock].collectedRings.size();
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_EP_GET_RING_SUCC);
|
||||
}
|
||||
|
||||
static void racingCancel(CNSocket* sock, CNPacketData* data) {
|
||||
if (EPRaces.find(sock) == EPRaces.end())
|
||||
return; // race not found
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
EPRaces.erase(sock);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
|
||||
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
|
||||
|
||||
/*
|
||||
* This request packet is used for both cancelling the race via the
|
||||
* NPC at the start, *and* failing the race by running out of time.
|
||||
* If the latter is to happen, the client disables movement until it
|
||||
* receives a packet from the server that re-enables it.
|
||||
*
|
||||
* So, in order to prevent a potential softlock we respawn the player.
|
||||
*/
|
||||
|
||||
WarpLocation* respawnLoc = PlayerManager::getRespawnPoint(plr);
|
||||
|
||||
if (respawnLoc != nullptr) {
|
||||
PlayerManager::sendPlayerTo(sock, respawnLoc->x, respawnLoc->y, respawnLoc->z, respawnLoc->instanceID);
|
||||
} else {
|
||||
// fallback, just respawn the player in-place if no suitable point is found
|
||||
PlayerManager::sendPlayerTo(sock, plr->x, plr->y, plr->z, plr->instanceID);
|
||||
}
|
||||
}
|
||||
|
||||
static void racingEnd(CNSocket* sock, CNPacketData* data) {
|
||||
if (EPRaces.find(sock) == EPRaces.end())
|
||||
return; // race not found
|
||||
|
||||
auto req = (sP_CL2FE_REQ_EP_RACE_END*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (NPCManager::NPCs.find(req->iEndEcomID) == NPCManager::NPCs.end())
|
||||
return; // finish line agent not found
|
||||
|
||||
int mapNum = MAPNUM(NPCManager::NPCs[req->iEndEcomID]->instanceID);
|
||||
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
|
||||
return; // IZ not found
|
||||
|
||||
EPInfo& epInfo = EPData[mapNum];
|
||||
EPRace& epRace = EPRaces[sock];
|
||||
|
||||
uint64_t now = getTime() / 1000;
|
||||
int timeDiff = now - epRace.startTime;
|
||||
int podsCollected = epRace.collectedRings.size();
|
||||
|
||||
int score = std::exp(
|
||||
(epInfo.podFactor * podsCollected) / epInfo.maxPods
|
||||
- (epInfo.timeFactor * timeDiff) / epInfo.maxTime
|
||||
+ epInfo.scaleFactor);
|
||||
score = (settings::IZRACESCORECAPPED && score > epInfo.maxScore) ? epInfo.maxScore : score;
|
||||
int fm = (1.0 + std::exp(epInfo.scaleFactor - 1.0) * epInfo.podFactor * podsCollected) / epInfo.maxPods;
|
||||
|
||||
// we submit the ranking first...
|
||||
Database::RaceRanking postRanking = {};
|
||||
postRanking.EPID = epInfo.EPID;
|
||||
postRanking.PlayerID = plr->iID;
|
||||
postRanking.RingCount = podsCollected;
|
||||
postRanking.Score = score;
|
||||
postRanking.Time = timeDiff;
|
||||
postRanking.Timestamp = getTimestamp();
|
||||
Database::postRaceRanking(postRanking);
|
||||
|
||||
// ...then we get the top ranking, which may or may not be what we just submitted
|
||||
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(epInfo.EPID, plr->iID);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
|
||||
|
||||
// get rank scores and rewards
|
||||
std::vector<int>* rankScores = &EPRewards[epInfo.EPID].first;
|
||||
std::vector<int>* rankRewards = &EPRewards[epInfo.EPID].second;
|
||||
|
||||
// top ranking
|
||||
int maxRank = rankScores->size() - 1;
|
||||
int topRank = 0;
|
||||
while (topRank < maxRank && rankScores->at(topRank) > topRankingPlayer.Score)
|
||||
topRank++;
|
||||
|
||||
resp.iEPTopRank = topRank + 1;
|
||||
resp.iEPTopRingCount = topRankingPlayer.RingCount;
|
||||
resp.iEPTopScore = topRankingPlayer.Score;
|
||||
resp.iEPTopTime = topRankingPlayer.Time;
|
||||
|
||||
// this ranking
|
||||
int rank = 0;
|
||||
while (rank < maxRank && rankScores->at(rank) > postRanking.Score)
|
||||
rank++;
|
||||
|
||||
resp.iEPRank = rank + 1;
|
||||
resp.iEPRingCnt = postRanking.RingCount;
|
||||
resp.iEPScore = postRanking.Score;
|
||||
resp.iEPRaceTime = postRanking.Time;
|
||||
resp.iEPRaceMode = EPRaces[sock].mode;
|
||||
resp.iEPRewardFM = fm;
|
||||
|
||||
Missions::updateFusionMatter(sock, resp.iEPRewardFM);
|
||||
|
||||
resp.iFusionMatter = plr->fusionmatter;
|
||||
resp.iFatigue = 50;
|
||||
resp.iFatigue_Level = 1;
|
||||
|
||||
sItemReward reward;
|
||||
reward.iSlotNum = Items::findFreeSlot(plr);
|
||||
reward.eIL = 1;
|
||||
sItemBase item;
|
||||
item.iID = rankRewards->at(rank); // rank scores and rewards line up
|
||||
item.iType = 9;
|
||||
item.iOpt = 1;
|
||||
item.iTimeLimit = 0;
|
||||
reward.sItem = item;
|
||||
|
||||
if (reward.iSlotNum > -1 && reward.sItem.iID != 0) {
|
||||
resp.RewardItem = reward;
|
||||
plr->Inven[reward.iSlotNum] = item;
|
||||
}
|
||||
|
||||
EPRaces.erase(sock);
|
||||
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_END_SUCC);
|
||||
}
|
||||
|
||||
void Racing::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_START, racingStart);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_GET_RING, racingGetPod);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_CANCEL, racingCancel);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_END, racingEnd);
|
||||
}
|
||||
29
src/Racing.hpp
Normal file
29
src/Racing.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
|
||||
struct EPInfo {
|
||||
// available through XDT (maxScore may be updated by drops)
|
||||
int zoneX, zoneY, EPID, maxScore;
|
||||
// available through drops
|
||||
int maxTime, maxPods;
|
||||
double scaleFactor, podFactor, timeFactor;
|
||||
};
|
||||
|
||||
struct EPRace {
|
||||
std::set<int> collectedRings;
|
||||
int mode, ticketSlot;
|
||||
time_t startTime;
|
||||
};
|
||||
|
||||
namespace Racing {
|
||||
extern std::map<int32_t, EPInfo> EPData;
|
||||
extern std::map<CNSocket*, EPRace> EPRaces;
|
||||
extern std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> EPRewards;
|
||||
|
||||
void init();
|
||||
}
|
||||
91
src/Rand.cpp
Normal file
91
src/Rand.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include "Rand.hpp"
|
||||
#include "core/Core.hpp"
|
||||
|
||||
std::unique_ptr<std::mt19937> Rand::generator;
|
||||
|
||||
int32_t Rand::rand(int32_t startInclusive, int32_t endExclusive) {
|
||||
std::uniform_int_distribution<int32_t> dist(startInclusive, endExclusive - 1);
|
||||
return dist(*Rand::generator);
|
||||
}
|
||||
|
||||
int32_t Rand::rand(int32_t endExclusive) {
|
||||
return Rand::rand(0, endExclusive);
|
||||
}
|
||||
|
||||
int32_t Rand::rand() {
|
||||
return Rand::rand(0, INT32_MAX);
|
||||
}
|
||||
|
||||
int32_t Rand::randWeighted(const std::vector<int32_t>& weights) {
|
||||
std::discrete_distribution<int32_t> dist(weights.begin(), weights.end());
|
||||
return dist(*Rand::generator);
|
||||
}
|
||||
|
||||
float Rand::randFloat(float startInclusive, float endExclusive) {
|
||||
std::uniform_real_distribution<float> dist(startInclusive, endExclusive);
|
||||
return dist(*Rand::generator);
|
||||
}
|
||||
|
||||
float Rand::randFloat(float endExclusive) {
|
||||
return Rand::randFloat(0.0f, endExclusive);
|
||||
}
|
||||
|
||||
float Rand::randFloat() {
|
||||
return Rand::randFloat(0.0f, 1.0f);
|
||||
}
|
||||
|
||||
#define RANDBYTES 8
|
||||
|
||||
/*
|
||||
* Cryptographically secure RNG. Borrowed from bcrypt_gensalt().
|
||||
*/
|
||||
uint64_t Rand::cryptoRand() {
|
||||
uint8_t buf[RANDBYTES];
|
||||
|
||||
#ifdef _WIN32
|
||||
HCRYPTPROV p;
|
||||
|
||||
// Acquire a crypt context for generating random bytes.
|
||||
if (CryptAcquireContext(&p, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT) == FALSE) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (CryptGenRandom(p, RANDBYTES, (BYTE*)buf) == FALSE) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (CryptReleaseContext(p, 0) == FALSE) {
|
||||
goto fail;
|
||||
}
|
||||
#else
|
||||
int fd;
|
||||
|
||||
// Get random bytes on Unix/Linux.
|
||||
fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd < 0) {
|
||||
perror("open");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (read(fd, buf, RANDBYTES) < RANDBYTES) {
|
||||
perror("read");
|
||||
close(fd);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
#endif
|
||||
|
||||
return *(uint64_t*)buf;
|
||||
|
||||
fail:
|
||||
std::cout << "[FATAL] Failed to generate cryptographic random number" << std::endl;
|
||||
terminate(0);
|
||||
|
||||
/* not reached */
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Rand::init(uint64_t seed) {
|
||||
Rand::generator = std::make_unique<std::mt19937>(std::mt19937(seed));
|
||||
}
|
||||
23
src/Rand.hpp
Normal file
23
src/Rand.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <random>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Rand {
|
||||
extern std::unique_ptr<std::mt19937> generator;
|
||||
|
||||
void init(uint64_t seed);
|
||||
|
||||
int32_t rand(int32_t startInclusive, int32_t endExclusive);
|
||||
int32_t rand(int32_t endExclusive);
|
||||
int32_t rand();
|
||||
|
||||
int32_t randWeighted(const std::vector<int32_t>& weights);
|
||||
|
||||
uint64_t cryptoRand();
|
||||
|
||||
float randFloat(float startInclusive, float endExclusive);
|
||||
float randFloat(float endExclusive);
|
||||
float randFloat();
|
||||
};
|
||||
1390
src/TableData.cpp
Normal file
1390
src/TableData.cpp
Normal file
File diff suppressed because it is too large
Load Diff
32
src/TableData.hpp
Normal file
32
src/TableData.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "JSON.hpp"
|
||||
|
||||
#include "Entities.hpp"
|
||||
#include "Transport.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// these are added to the NPC's static key to avoid collisions
|
||||
const int NPC_ID_OFFSET = 1;
|
||||
const int MOB_ID_OFFSET = 10000;
|
||||
const int MOB_GROUP_ID_OFFSET = 20000;
|
||||
|
||||
// typedef for JSON object because I don't want to type nlohmann::json every time
|
||||
typedef nlohmann::json json;
|
||||
|
||||
namespace TableData {
|
||||
extern std::map<int32_t, std::vector<Vec3>> RunningSkywayRoutes;
|
||||
extern std::map<int32_t, int> RunningNPCRotations;
|
||||
extern std::map<int32_t, int> RunningNPCMapNumbers;
|
||||
extern std::unordered_map<int32_t, std::pair<BaseNPC*, std::vector<BaseNPC*>>> RunningNPCPaths; // player ID -> following NPC
|
||||
extern std::vector<NPCPath> FinishedNPCPaths; // NPC ID -> path
|
||||
extern std::map<int32_t, BaseNPC*> RunningMobs;
|
||||
extern std::map<int32_t, BaseNPC*> RunningGroups;
|
||||
extern std::map<int32_t, BaseNPC*> RunningEggs;
|
||||
|
||||
void init();
|
||||
void flush();
|
||||
}
|
||||
470
src/Trading.cpp
Normal file
470
src/Trading.cpp
Normal file
@@ -0,0 +1,470 @@
|
||||
#include "Trading.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "PlayerManager.hpp"
|
||||
#include "db/Database.hpp"
|
||||
|
||||
using namespace Trading;
|
||||
|
||||
static bool doTrade(Player* plr, Player* plr2) {
|
||||
// init dummy inventories
|
||||
sItemBase plrInven[AINVEN_COUNT];
|
||||
sItemBase plr2Inven[AINVEN_COUNT];
|
||||
memcpy(plrInven, plr->Inven, AINVEN_COUNT * sizeof(sItemBase));
|
||||
memcpy(plr2Inven, plr2->Inven, AINVEN_COUNT * sizeof(sItemBase));
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// remove items offered by us
|
||||
if (plr->Trade[i].iID != 0) {
|
||||
if (plrInven[plr->Trade[i].iInvenNum].iID == 0
|
||||
|| plr->Trade[i].iID != plrInven[plr->Trade[i].iInvenNum].iID
|
||||
|| plr->Trade[i].iType != plrInven[plr->Trade[i].iInvenNum].iType) // pulling a fast one on us
|
||||
return false;
|
||||
|
||||
if (plr->Trade[i].iOpt < 1) {
|
||||
std::cout << "[WARN] Player tried trading an iOpt < 1 amount" << std::endl;
|
||||
plr->Trade[i].iOpt = 1;
|
||||
}
|
||||
|
||||
// for stacked items
|
||||
plrInven[plr->Trade[i].iInvenNum].iOpt -= plr->Trade[i].iOpt;
|
||||
|
||||
if (plrInven[plr->Trade[i].iInvenNum].iOpt == 0) {
|
||||
plrInven[plr->Trade[i].iInvenNum].iID = 0;
|
||||
plrInven[plr->Trade[i].iInvenNum].iType = 0;
|
||||
plrInven[plr->Trade[i].iInvenNum].iOpt = 0;
|
||||
} else if (plrInven[plr->Trade[i].iInvenNum].iOpt < 0) { // another dupe attempt
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (plr2->Trade[i].iID != 0) {
|
||||
if (plr2Inven[plr2->Trade[i].iInvenNum].iID == 0
|
||||
|| plr2->Trade[i].iID != plr2Inven[plr2->Trade[i].iInvenNum].iID
|
||||
|| plr2->Trade[i].iType != plr2Inven[plr2->Trade[i].iInvenNum].iType) // pulling a fast one on us
|
||||
return false;
|
||||
|
||||
if (plr2->Trade[i].iOpt < 1) {
|
||||
std::cout << "[WARN] Player tried trading an iOpt < 1 amount" << std::endl;
|
||||
plr2->Trade[i].iOpt = 1;
|
||||
}
|
||||
|
||||
// for stacked items
|
||||
plr2Inven[plr2->Trade[i].iInvenNum].iOpt -= plr2->Trade[i].iOpt;
|
||||
|
||||
if (plr2Inven[plr2->Trade[i].iInvenNum].iOpt == 0) {
|
||||
plr2Inven[plr2->Trade[i].iInvenNum].iID = 0;
|
||||
plr2Inven[plr2->Trade[i].iInvenNum].iType = 0;
|
||||
plr2Inven[plr2->Trade[i].iInvenNum].iOpt = 0;
|
||||
} else if (plr2Inven[plr2->Trade[i].iInvenNum].iOpt < 0) { // another dupe attempt
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// add items offered to us
|
||||
if (plr2->Trade[i].iID != 0) {
|
||||
for (int n = 0; n < AINVEN_COUNT; n++) {
|
||||
if (plrInven[n].iID == 0) {
|
||||
plrInven[n].iID = plr2->Trade[i].iID;
|
||||
plrInven[n].iType = plr2->Trade[i].iType;
|
||||
plrInven[n].iOpt = plr2->Trade[i].iOpt;
|
||||
plr2->Trade[i].iInvenNum = n;
|
||||
break;
|
||||
}
|
||||
|
||||
if (n >= AINVEN_COUNT - 1)
|
||||
return false; // not enough space
|
||||
}
|
||||
}
|
||||
|
||||
if (plr->Trade[i].iID != 0) {
|
||||
for (int n = 0; n < AINVEN_COUNT; n++) {
|
||||
if (plr2Inven[n].iID == 0) {
|
||||
plr2Inven[n].iID = plr->Trade[i].iID;
|
||||
plr2Inven[n].iType = plr->Trade[i].iType;
|
||||
plr2Inven[n].iOpt = plr->Trade[i].iOpt;
|
||||
plr->Trade[i].iInvenNum = n;
|
||||
break;
|
||||
}
|
||||
|
||||
if (n >= AINVEN_COUNT - 1)
|
||||
return false; // not enough space
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if everything went well, back into player inventory it goes
|
||||
memcpy(plr->Inven, plrInven, AINVEN_COUNT * sizeof(sItemBase));
|
||||
memcpy(plr2->Inven, plr2Inven, AINVEN_COUNT * sizeof(sItemBase));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void tradeOffer(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_OFFER* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
if (plr->isTrading) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, resp);
|
||||
resp.iID_Request = pacdat->iID_To;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
|
||||
return; // prevent trading with a player already trading
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER));
|
||||
}
|
||||
|
||||
static void tradeOfferAccept(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
if (plr2->isTrading) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, resp);
|
||||
resp.iID_Request = pacdat->iID_From;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
|
||||
return; // prevent trading with a player already trading
|
||||
}
|
||||
|
||||
// clearing up trade slots
|
||||
plr->moneyInTrade = 0;
|
||||
plr2->moneyInTrade = 0;
|
||||
memset(&plr->Trade, 0, sizeof(plr->Trade));
|
||||
memset(&plr2->Trade, 0, sizeof(plr2->Trade));
|
||||
|
||||
// marking players as traders
|
||||
plr->isTrading = true;
|
||||
plr2->isTrading = true;
|
||||
|
||||
// marking players as unconfirmed
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
// inform the other player that offer is accepted
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_SUCC));
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_SUCC));
|
||||
}
|
||||
|
||||
static void tradeOfferRefusal(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
|
||||
}
|
||||
|
||||
static void tradeOfferCancel(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL));
|
||||
}
|
||||
|
||||
static void tradeOfferAbort(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT));
|
||||
}
|
||||
|
||||
static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf;
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
if (!(plr->isTrading && plr2->isTrading)) { // both players must be trading
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp);
|
||||
resp.iID_Request = plr2->iID;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
|
||||
resp.iID_Request = plr->iID;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
|
||||
|
||||
// both players are no longer trading
|
||||
plr->isTrading = false;
|
||||
plr2->isTrading = false;
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// send the confirm packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM));
|
||||
|
||||
if (!(plr2->isTradeConfirm)) {
|
||||
plr->isTradeConfirm = true;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM));
|
||||
return;
|
||||
}
|
||||
|
||||
// both players are no longer trading
|
||||
plr->isTrading = false;
|
||||
plr2->isTrading = false;
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
if (doTrade(plr, plr2)) { // returns false if not enough slots
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, resp2);
|
||||
resp2.iID_Request = pacdat->iID_Request;
|
||||
resp2.iID_From = pacdat->iID_From;
|
||||
resp2.iID_To = pacdat->iID_To;
|
||||
plr->money = plr->money + plr2->moneyInTrade - plr->moneyInTrade;
|
||||
resp2.iCandy = plr->money;
|
||||
memcpy(resp2.Item, plr2->Trade, sizeof(plr2->Trade));
|
||||
memcpy(resp2.ItemStay, plr->Trade, sizeof(plr->Trade));
|
||||
|
||||
sock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
|
||||
|
||||
plr2->money = plr2->money + plr->moneyInTrade - plr2->moneyInTrade;
|
||||
resp2.iCandy = plr2->money;
|
||||
memcpy(resp2.Item, plr->Trade, sizeof(plr->Trade));
|
||||
memcpy(resp2.ItemStay, plr2->Trade, sizeof(plr2->Trade));
|
||||
|
||||
otherSock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
|
||||
} else {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp);
|
||||
resp.iID_Request = plr->iID;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
|
||||
resp.iID_Request = plr2->iID;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
|
||||
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
|
||||
std::string text = "Trade Failed";
|
||||
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
|
||||
msg.iDuringTime = 3;
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
otherSock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
return;
|
||||
}
|
||||
|
||||
Database::commitTrade(plr, plr2);
|
||||
}
|
||||
|
||||
static void tradeConfirmCancel(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL*)data->buf;
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
// both players are not trading nor are in a confirmed state
|
||||
plr->isTrading = false;
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTrading = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL));
|
||||
}
|
||||
|
||||
static void tradeRegisterItem(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_ITEM_REGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_ITEM_REGISTER*)data->buf;
|
||||
|
||||
if (pacdat->Item.iSlotNum < 0 || pacdat->Item.iSlotNum > 4)
|
||||
return; // sanity check, there are only 5 trade slots
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
plr->Trade[pacdat->Item.iSlotNum] = pacdat->Item;
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
// since you can spread items like gumballs over multiple slots, we need to count them all
|
||||
// to make sure the inventory shows the right value during trade.
|
||||
int count = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (plr->Trade[i].iInvenNum == pacdat->Item.iInvenNum)
|
||||
count += plr->Trade[i].iOpt;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
resp.TradeItem = pacdat->Item;
|
||||
resp.InvenItem = pacdat->Item;
|
||||
resp.InvenItem.iOpt = plr->Inven[pacdat->Item.iInvenNum].iOpt - count; // subtract this count
|
||||
|
||||
if (resp.InvenItem.iOpt < 0) // negative count items, doTrade() will block this later on
|
||||
std::cout << "[WARN] tradeRegisterItem: an item went negative count client side." << std::endl;
|
||||
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC));
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC));
|
||||
}
|
||||
|
||||
static void tradeUnregisterItem(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER*)data->buf;
|
||||
|
||||
if (pacdat->Item.iSlotNum < 0 || pacdat->Item.iSlotNum > 4)
|
||||
return; // sanity check, there are only 5 trade slots
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
resp.TradeItem = pacdat->Item;
|
||||
resp.InvenItem = plr->Trade[pacdat->Item.iSlotNum];
|
||||
|
||||
memset(&plr->Trade[pacdat->Item.iSlotNum], 0, sizeof(plr->Trade[pacdat->Item.iSlotNum])); // clean up item slot
|
||||
|
||||
// since you can spread items like gumballs over multiple slots, we need to count them all
|
||||
// to make sure the inventory shows the right value during trade.
|
||||
int count = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (plr->Trade[i].iInvenNum == resp.InvenItem.iInvenNum)
|
||||
count += plr->Trade[i].iOpt;
|
||||
}
|
||||
|
||||
resp.InvenItem.iOpt = plr->Inven[resp.InvenItem.iInvenNum].iOpt - count; // subtract this count
|
||||
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC));
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC));
|
||||
}
|
||||
|
||||
static void tradeRegisterCash(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_CASH_REGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_CASH_REGISTER*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (pacdat->iCandy < 0 || pacdat->iCandy > plr->money)
|
||||
return; // famous glitch, begone
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
resp.iCandy = pacdat->iCandy;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
|
||||
|
||||
plr->moneyInTrade = pacdat->iCandy;
|
||||
plr->isTradeConfirm = false;
|
||||
}
|
||||
|
||||
void Trading::init() {
|
||||
// Trade handlers
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, tradeOffer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT, tradeOfferAccept);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, tradeOfferRefusal);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL, tradeOfferCancel);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ABORT, tradeOfferAbort);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM, tradeConfirm);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER, tradeUnregisterItem);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CASH_REGISTER, tradeRegisterCash);
|
||||
}
|
||||
5
src/Trading.hpp
Normal file
5
src/Trading.hpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace Trading {
|
||||
void init();
|
||||
}
|
||||
428
src/Transport.cpp
Normal file
428
src/Transport.cpp
Normal file
@@ -0,0 +1,428 @@
|
||||
#include "Transport.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "TableData.hpp"
|
||||
#include "Entities.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
#include "MobAI.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <cmath>
|
||||
|
||||
using namespace Transport;
|
||||
|
||||
std::map<int32_t, TransportRoute> Transport::Routes;
|
||||
std::map<int32_t, TransportLocation> Transport::Locations;
|
||||
std::vector<NPCPath> Transport::NPCPaths;
|
||||
std::map<int32_t, std::queue<Vec3>> Transport::SkywayPaths;
|
||||
std::unordered_map<CNSocket*, std::queue<Vec3>> Transport::SkywayQueues;
|
||||
std::unordered_map<int32_t, std::queue<Vec3>> Transport::NPCQueues;
|
||||
|
||||
static void transportRegisterLocationHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto transport = (sP_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
bool newReg = false; // this is a new registration
|
||||
//std::cout << "request to register transport, eTT " << transport->eTT << ", locID " << transport->iLocationID << ", npc " << transport->iNPC_ID << std::endl;
|
||||
if (transport->eTT == 1) { // S.C.A.M.P.E.R.
|
||||
if (transport->iLocationID < 1 || transport->iLocationID > 31) { // sanity check
|
||||
std::cout << "[WARN] S.C.A.M.P.E.R. location ID " << transport->iLocationID << " is out of bounds" << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);
|
||||
|
||||
failResp.eTT = transport->eTT;
|
||||
failResp.iErrorCode = 0; // TODO: review what error code to use here
|
||||
failResp.iLocationID = transport->iLocationID;
|
||||
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// update registration bitfield using bitmask
|
||||
uint32_t newScamperFlag = plr->iWarpLocationFlag | (1UL << (transport->iLocationID - 1));
|
||||
if (newScamperFlag != plr->iWarpLocationFlag) {
|
||||
plr->iWarpLocationFlag = newScamperFlag;
|
||||
newReg = true;
|
||||
}
|
||||
} else if (transport->eTT == 2) { // Monkey Skyway System
|
||||
if (transport->iLocationID < 1 || transport->iLocationID > 127) { // sanity check
|
||||
std::cout << "[WARN] Skyway location ID " << transport->iLocationID << " is out of bounds" << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);
|
||||
|
||||
failResp.eTT = transport->eTT;
|
||||
failResp.iErrorCode = 0; // TODO: review what error code to use here
|
||||
failResp.iLocationID = transport->iLocationID;
|
||||
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* assuming the two bitfields are just stuck together to make a longer one, do a similar operation
|
||||
*/
|
||||
int index = transport->iLocationID > 64 ? 1 : 0;
|
||||
uint64_t newMonkeyFlag = plr->aSkywayLocationFlag[index] | (1ULL << (index ? transport->iLocationID - 65 : transport->iLocationID - 1));
|
||||
if (newMonkeyFlag != plr->aSkywayLocationFlag[index]) {
|
||||
plr->aSkywayLocationFlag[index] = newMonkeyFlag;
|
||||
newReg = true;
|
||||
}
|
||||
} else {
|
||||
std::cout << "[WARN] Unknown mode of transport; eTT = " << transport->eTT << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);
|
||||
|
||||
failResp.eTT = transport->eTT;
|
||||
failResp.iErrorCode = 0; // TODO: review what error code to use here
|
||||
failResp.iLocationID = transport->iLocationID;
|
||||
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newReg)
|
||||
return; // don't send new registration message
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC, resp);
|
||||
// response parameters
|
||||
resp.eTT = transport->eTT;
|
||||
resp.iLocationID = transport->iLocationID;
|
||||
resp.iWarpLocationFlag = plr->iWarpLocationFlag;
|
||||
resp.aWyvernLocationFlag[0] = plr->aSkywayLocationFlag[0];
|
||||
resp.aWyvernLocationFlag[1] = plr->aSkywayLocationFlag[1];
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC);
|
||||
}
|
||||
|
||||
static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
/*
|
||||
* req:
|
||||
* eIL -- inventory type
|
||||
* iNPC_ID -- the ID of the NPC who is warping you
|
||||
* iTransporationID -- iVehicleID
|
||||
* iSlotNum -- inventory slot number
|
||||
*/
|
||||
|
||||
if (Routes.find(req->iTransporationID) == Routes.end() || Routes[req->iTransporationID].cost > plr->money) { // sanity check
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL, failResp);
|
||||
|
||||
failResp.iErrorCode = 0; // TODO: error code
|
||||
failResp.iTransportationID = req->iTransporationID;
|
||||
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
TransportRoute route = Routes[req->iTransporationID];
|
||||
plr->money -= route.cost;
|
||||
|
||||
TransportLocation* target = nullptr;
|
||||
switch (route.type) {
|
||||
case 1: // S.C.A.M.P.E.R.
|
||||
target = &Locations[route.end];
|
||||
break;
|
||||
case 2: // Monkey Skyway
|
||||
// set last safe coords
|
||||
plr->lastX = plr->x;
|
||||
plr->lastY = plr->y;
|
||||
plr->lastZ = plr->z;
|
||||
if (SkywayPaths.find(route.mssRouteNum) != SkywayPaths.end()) { // check if route exists
|
||||
Nanos::summonNano(sock, -1); // make sure that no nano is active during the ride
|
||||
SkywayQueues[sock] = SkywayPaths[route.mssRouteNum]; // set socket point queue to route
|
||||
plr->onMonkey = true;
|
||||
break;
|
||||
} else if (TableData::RunningSkywayRoutes.find(route.mssRouteNum) != TableData::RunningSkywayRoutes.end()) {
|
||||
std::vector<Vec3>* _route = &TableData::RunningSkywayRoutes[route.mssRouteNum];
|
||||
Nanos::summonNano(sock, -1);
|
||||
testMssRoute(sock, _route);
|
||||
plr->onMonkey = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// refund and send alert packet
|
||||
plr->money += route.cost;
|
||||
INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert);
|
||||
alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog
|
||||
alert.iDuringTime = 3;
|
||||
U8toU16("Skyway route " + std::to_string(route.mssRouteNum) + " isn't pathed yet. You will not be charged any taros.", (char16_t*)alert.szAnnounceMsg, sizeof(alert.szAnnounceMsg));
|
||||
sock->sendPacket(alert, P_FE2CL_ANNOUNCE_MSG);
|
||||
|
||||
std::cout << "[WARN] MSS route " << route.mssRouteNum << " not pathed" << std::endl;
|
||||
break;
|
||||
default:
|
||||
std::cout << "[WARN] Unknown tranportation type " << route.type << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC, resp);
|
||||
// response parameters
|
||||
resp.eTT = route.type;
|
||||
resp.iCandy = plr->money;
|
||||
resp.iX = (target == nullptr) ? plr->x : target->x;
|
||||
resp.iY = (target == nullptr) ? plr->y : target->y;
|
||||
resp.iZ = (target == nullptr) ? plr->z : target->z;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC);
|
||||
|
||||
if (target == nullptr)
|
||||
return;
|
||||
|
||||
// we warped; update position and chunks
|
||||
PlayerManager::updatePlayerPositionForWarp(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD);
|
||||
}
|
||||
|
||||
void Transport::testMssRoute(CNSocket *sock, std::vector<Vec3>* route) {
|
||||
int speed = 1500; // TODO: make this adjustable
|
||||
std::queue<Vec3> path;
|
||||
Vec3 last = route->front(); // start pos
|
||||
|
||||
for (int i = 1; i < route->size(); i++) {
|
||||
Vec3 coords = route->at(i);
|
||||
Transport::lerp(&path, last, coords, speed);
|
||||
path.push(coords); // add keyframe to the queue
|
||||
last = coords; // update start pos
|
||||
}
|
||||
|
||||
SkywayQueues[sock] = path;
|
||||
}
|
||||
|
||||
/*
|
||||
* Go through every socket that has broomstick points queued up, and advance to the next point.
|
||||
* If the player has disconnected or finished the route, clean up and remove them from the queue.
|
||||
*/
|
||||
static void stepSkywaySystem() {
|
||||
|
||||
// using an unordered map so we can remove finished players in one iteration
|
||||
std::unordered_map<CNSocket*, std::queue<Vec3>>::iterator it = SkywayQueues.begin();
|
||||
while (it != SkywayQueues.end()) {
|
||||
|
||||
std::queue<Vec3>* queue = &it->second;
|
||||
|
||||
if (PlayerManager::players.find(it->first) == PlayerManager::players.end()) {
|
||||
// pluck out dead socket + update iterator
|
||||
it = SkywayQueues.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(it->first);
|
||||
|
||||
if (queue->empty()) {
|
||||
// send dismount packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_RIDING_SUCC, rideSucc);
|
||||
INITSTRUCT(sP_FE2CL_PC_RIDING, rideBroadcast);
|
||||
rideSucc.iPC_ID = plr->iID;
|
||||
rideSucc.eRT = 0;
|
||||
rideBroadcast.iPC_ID = plr->iID;
|
||||
rideBroadcast.eRT = 0;
|
||||
it->first->sendPacket(rideSucc, P_FE2CL_REP_PC_RIDING_SUCC);
|
||||
// send packet to players in view
|
||||
PlayerManager::sendToViewable(it->first, rideBroadcast, P_FE2CL_PC_RIDING);
|
||||
it = SkywayQueues.erase(it); // remove player from tracking map + update iterator
|
||||
plr->onMonkey = false;
|
||||
} else {
|
||||
Vec3 point = queue->front(); // get point
|
||||
queue->pop(); // remove point from front of queue
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_BROOMSTICK_MOVE, bmstk);
|
||||
bmstk.iPC_ID = plr->iID;
|
||||
bmstk.iToX = point.x;
|
||||
bmstk.iToY = point.y;
|
||||
bmstk.iToZ = point.z;
|
||||
it->first->sendPacket(bmstk, P_FE2CL_PC_BROOMSTICK_MOVE);
|
||||
// set player location to point to update viewables
|
||||
PlayerManager::updatePlayerPosition(it->first, point.x, point.y, point.z, plr->instanceID, plr->angle);
|
||||
// send packet to players in view
|
||||
PlayerManager::sendToViewable(it->first, bmstk, P_FE2CL_PC_BROOMSTICK_MOVE);
|
||||
|
||||
it++; // go to next entry in map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void stepNPCPathing() {
|
||||
|
||||
// all NPC pathing queues
|
||||
std::unordered_map<int32_t, std::queue<Vec3>>::iterator it = NPCQueues.begin();
|
||||
while (it != NPCQueues.end()) {
|
||||
|
||||
std::queue<Vec3>* queue = &it->second;
|
||||
|
||||
BaseNPC* npc = nullptr;
|
||||
if (NPCManager::NPCs.find(it->first) != NPCManager::NPCs.end())
|
||||
npc = NPCManager::NPCs[it->first];
|
||||
|
||||
if (npc == nullptr || queue->empty()) {
|
||||
// pluck out dead path + update iterator
|
||||
it = NPCQueues.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip if not simulating mobs
|
||||
if (npc->kind == EntityKind::MOB && !MobAI::simulateMobs) {
|
||||
it++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// do not roam if not roaming
|
||||
if (npc->kind == EntityKind::MOB && ((Mob*)npc)->state != AIState::ROAMING) {
|
||||
it++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Vec3 point = queue->front(); // get point
|
||||
queue->pop(); // remove point from front of queue
|
||||
|
||||
// calculate displacement
|
||||
int dXY = hypot(point.x - npc->x, point.y - npc->y); // XY plane distance
|
||||
int distanceBetween = hypot(dXY, point.z - npc->z); // total distance
|
||||
|
||||
// update NPC location to update viewables
|
||||
NPCManager::updateNPCPosition(npc->id, point.x, point.y, point.z, npc->instanceID, npc->angle);
|
||||
|
||||
// TODO: move walking logic into Entity stack
|
||||
switch (npc->kind) {
|
||||
case EntityKind::BUS:
|
||||
INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove);
|
||||
|
||||
busMove.eTT = 3;
|
||||
busMove.iT_ID = npc->id;
|
||||
busMove.iMoveStyle = 0; // ???
|
||||
busMove.iToX = point.x;
|
||||
busMove.iToY = point.y;
|
||||
busMove.iToZ = point.z;
|
||||
busMove.iSpeed = distanceBetween; // set to distance to match how monkeys work
|
||||
|
||||
NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE));
|
||||
break;
|
||||
case EntityKind::MOB:
|
||||
MobAI::incNextMovement((Mob*)npc);
|
||||
/* fallthrough */
|
||||
default:
|
||||
INITSTRUCT(sP_FE2CL_NPC_MOVE, move);
|
||||
move.iNPC_ID = npc->id;
|
||||
move.iMoveStyle = 0; // ???
|
||||
move.iToX = point.x;
|
||||
move.iToY = point.y;
|
||||
move.iToZ = point.z;
|
||||
move.iSpeed = distanceBetween;
|
||||
|
||||
NPCManager::sendToViewable(npc, &move, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* If this path should be repeated, move processed point to the back to maintain cycle.
|
||||
*/
|
||||
if (npc->loopingPath)
|
||||
queue->push(point);
|
||||
|
||||
it++; // go to next entry in map
|
||||
}
|
||||
}
|
||||
|
||||
static void tickTransportationSystem(CNServer* serv, time_t currTime) {
|
||||
stepNPCPathing();
|
||||
stepSkywaySystem();
|
||||
}
|
||||
|
||||
/*
|
||||
* Linearly interpolate between two points and insert the results into a queue.
|
||||
*/
|
||||
void Transport::lerp(std::queue<Vec3>* queue, Vec3 start, Vec3 end, int gapSize, float curve) {
|
||||
int dXY = hypot(end.x - start.x, end.y - start.y); // XY plane distance
|
||||
int distanceBetween = hypot(dXY, end.z - start.z); // total distance
|
||||
int lerps = distanceBetween / gapSize; // number of intermediate points to add
|
||||
for (int i = 1; i <= lerps; i++) {
|
||||
Vec3 lerp;
|
||||
// lerp math
|
||||
//float frac = i / (lerps + 1);
|
||||
float frac = powf(i, curve) / powf(lerps + 1, curve);
|
||||
lerp.x = (start.x * (1.0f - frac)) + (end.x * frac);
|
||||
lerp.y = (start.y * (1.0f - frac)) + (end.y * frac);
|
||||
lerp.z = (start.z * (1.0f - frac)) + (end.z * frac);
|
||||
queue->push(lerp); // add lerp'd point
|
||||
}
|
||||
}
|
||||
void Transport::lerp(std::queue<Vec3>* queue, Vec3 start, Vec3 end, int gapSize) {
|
||||
lerp(queue, start, end, gapSize, 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Find and return the first path that targets either the type or the ID.
|
||||
* If no matches are found, return nullptr
|
||||
*/
|
||||
NPCPath* Transport::findApplicablePath(int32_t id, int32_t type, int taskID) {
|
||||
NPCPath* match = nullptr;
|
||||
for (auto _path = Transport::NPCPaths.begin(); _path != Transport::NPCPaths.end(); _path++) {
|
||||
|
||||
// task ID for the path must match so escorts don't start early
|
||||
if (_path->escortTaskID != taskID)
|
||||
continue;
|
||||
|
||||
// search target IDs
|
||||
for (int32_t pID : _path->targetIDs) {
|
||||
if (id == pID) {
|
||||
match = &(*_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match != nullptr)
|
||||
break; // early break for ID matches, since ID has higher priority than type
|
||||
|
||||
// search target types
|
||||
for (int32_t pType : _path->targetTypes) {
|
||||
if (type == pType) {
|
||||
match = &(*_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match != nullptr)
|
||||
break;
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
void Transport::constructPathNPC(int32_t id, NPCPath* path) {
|
||||
BaseNPC* npc = NPCManager::NPCs[id];
|
||||
if (npc->kind == EntityKind::MOB)
|
||||
((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);
|
||||
}
|
||||
59
src/Transport.hpp
Normal file
59
src/Transport.hpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
|
||||
const int SLIDER_SPEED = 1200;
|
||||
const int SLIDER_STOP_TICKS = 16;
|
||||
const int SLIDER_GAP_SIZE = 45000;
|
||||
|
||||
const int NPC_DEFAULT_SPEED = 300;
|
||||
|
||||
struct Vec3 {
|
||||
int x, y, z;
|
||||
};
|
||||
|
||||
struct WarpLocation {
|
||||
int x, y, z, instanceID, isInstance, limitTaskID, npcID;
|
||||
};
|
||||
|
||||
struct TransportRoute {
|
||||
int type, start, end, cost, mssSpeed, mssRouteNum;
|
||||
};
|
||||
|
||||
struct TransportLocation {
|
||||
int npcID, x, y, z;
|
||||
};
|
||||
|
||||
struct NPCPath {
|
||||
std::vector<Vec3> points;
|
||||
std::vector<int32_t> targetIDs;
|
||||
std::vector<int32_t> targetTypes;
|
||||
int speed;
|
||||
int escortTaskID;
|
||||
bool isRelative;
|
||||
bool isLoop;
|
||||
};
|
||||
|
||||
namespace Transport {
|
||||
extern std::map<int32_t, TransportRoute> Routes;
|
||||
extern std::map<int32_t, TransportLocation> Locations;
|
||||
extern std::vector<NPCPath> NPCPaths; // predefined NPC paths
|
||||
extern std::map<int32_t, std::queue<Vec3>> SkywayPaths; // predefined skyway paths with points
|
||||
extern std::unordered_map<CNSocket*, std::queue<Vec3>> SkywayQueues; // player sockets with queued broomstick points
|
||||
extern std::unordered_map<int32_t, std::queue<Vec3>> NPCQueues; // NPC ids with queued pathing points
|
||||
|
||||
void init();
|
||||
|
||||
void testMssRoute(CNSocket *sock, std::vector<Vec3>* route);
|
||||
|
||||
void lerp(std::queue<Vec3>*, Vec3, Vec3, int, float);
|
||||
void lerp(std::queue<Vec3>*, Vec3, Vec3, int);
|
||||
|
||||
NPCPath* findApplicablePath(int32_t, int32_t, int = -1);
|
||||
void constructPathNPC(int32_t, NPCPath*);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "CNShardServer.hpp"
|
||||
|
||||
namespace TransportManager {
|
||||
void init();
|
||||
|
||||
void transportRegisterLocationHandler(CNSocket* sock, CNPacketData* data);
|
||||
}
|
||||
405
src/Vendors.cpp
Normal file
405
src/Vendors.cpp
Normal file
@@ -0,0 +1,405 @@
|
||||
#include "Vendors.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "Rand.hpp"
|
||||
|
||||
// 7 days
|
||||
#define VEHICLE_EXPIRY_DURATION 604800
|
||||
|
||||
using namespace Vendors;
|
||||
|
||||
std::map<int32_t, std::vector<VendorListing>> Vendors::VendorTables;
|
||||
|
||||
static void vendorBuy(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_BUY*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// prepare fail packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp);
|
||||
failResp.iErrorCode = 0;
|
||||
|
||||
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end()) {
|
||||
std::cout << "[WARN] Vendor with ID " << req->iVendorID << " mismatched or not found (buy)" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<VendorListing>* listings = &Vendors::VendorTables[req->iVendorID];
|
||||
VendorListing reqItem;
|
||||
reqItem.id = req->Item.iID;
|
||||
reqItem.type = req->Item.iType;
|
||||
reqItem.sort = 0; // just to be safe
|
||||
|
||||
if (std::find(listings->begin(), listings->end(), reqItem) == listings->end()) { // item not found in listing
|
||||
std::cout << "[WARN] Player " << PlayerManager::getPlayerName(plr) << " tried to buy an item that wasn't on sale" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
Items::Item* itemDat = Items::getItemData(req->Item.iID, req->Item.iType);
|
||||
if (itemDat == nullptr) {
|
||||
std::cout << "[WARN] Item id " << req->Item.iID << " with type " << req->Item.iType << " not found (buy)" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
int itemCost = itemDat->buyPrice * (itemDat->stackSize > 1 ? req->Item.iOpt : 1);
|
||||
int slot = Items::findFreeSlot(plr);
|
||||
if (itemCost > plr->money || slot == -1) {
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// crates don't have a stack size in TableData, so we can't check those
|
||||
if (itemDat->stackSize != 0 && req->Item.iOpt > itemDat->stackSize) {
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// if vehicle
|
||||
if (req->Item.iType == 10) {
|
||||
// set time limit: current time + expiry duration
|
||||
req->Item.iTimeLimit = getTimestamp() + VEHICLE_EXPIRY_DURATION;
|
||||
}
|
||||
|
||||
if (slot != req->iInvenSlotNum) {
|
||||
// possible item stacking?
|
||||
std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC, resp);
|
||||
|
||||
plr->money = plr->money - itemCost;
|
||||
plr->Inven[slot] = req->Item;
|
||||
|
||||
resp.iCandy = plr->money;
|
||||
resp.iInvenSlotNum = slot;
|
||||
resp.Item = req->Item;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC);
|
||||
}
|
||||
|
||||
static void vendorSell(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_SELL*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// prepare a fail packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL, failResp);
|
||||
failResp.iErrorCode = 0;
|
||||
|
||||
if (req->iInvenSlotNum < 0 || req->iInvenSlotNum >= AINVEN_COUNT || req->iItemCnt < 0) {
|
||||
std::cout << "[WARN] Client failed to sell item in slot " << req->iInvenSlotNum << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
sItemBase* item = &plr->Inven[req->iInvenSlotNum];
|
||||
Items::Item* itemData = Items::getItemData(item->iID, item->iType);
|
||||
|
||||
if (itemData == nullptr || !itemData->sellable || item->iOpt < req->iItemCnt) { // sanity + sellable check
|
||||
std::cout << "[WARN] Item id " << item->iID << " with type " << item->iType << " not found (sell)" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// fail to sell croc-potted items
|
||||
if (item->iOpt >= 1 << 16) {
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
sItemBase original;
|
||||
memcpy(&original, item, sizeof(sItemBase));
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, resp);
|
||||
|
||||
// increment taros
|
||||
plr->money += itemData->sellPrice * req->iItemCnt;
|
||||
|
||||
// modify item
|
||||
if (item->iOpt - req->iItemCnt > 0) { // selling part of a stack
|
||||
item->iOpt -= req->iItemCnt;
|
||||
original.iOpt = req->iItemCnt;
|
||||
}
|
||||
else { // selling entire slot
|
||||
// make sure it's fully zeroed, even the padding and non-104 members
|
||||
memset(item, 0, sizeof(*item));
|
||||
}
|
||||
|
||||
// add to buyback list
|
||||
plr->buyback.push_back(original);
|
||||
// forget oldest member if there's more than 5
|
||||
if (plr->buyback.size() > 5)
|
||||
plr->buyback.erase(plr->buyback.begin());
|
||||
//std::cout << (int)plr->buyback.size() << " items in buyback\n";
|
||||
|
||||
// response parameters
|
||||
resp.iInvenSlotNum = req->iInvenSlotNum;
|
||||
resp.iCandy = plr->money;
|
||||
resp.Item = original; // the item that gets sent to buyback
|
||||
resp.ItemStay = *item; // the void item that gets put in the slot
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC);
|
||||
}
|
||||
|
||||
static void vendorBuyback(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// prepare fail packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, failResp);
|
||||
failResp.iErrorCode = 0;
|
||||
|
||||
//std::cout << "buying back from index " << (int)req->iListID << " into " << (int)req->iInvenSlotNum <<
|
||||
// " from " << (int)req->iNPC_ID << " (vendor = " << (int)req->iVendorID << ")\n";
|
||||
|
||||
int idx = req->iListID - 1;
|
||||
|
||||
// sanity check
|
||||
if (idx < 0 || idx >= plr->buyback.size()) {
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// get the item out of the buyback list
|
||||
sItemBase item = plr->buyback[idx];
|
||||
/*
|
||||
* NOTE: The client sends the index of the exact item the user clicked on.
|
||||
* We then operate on that item, but we remove the *first* identical item
|
||||
* from the buyback list, instead of the one at the supplied index.
|
||||
*
|
||||
* This was originally a mistake on my part, but it turns out the client
|
||||
* does the exact same thing, so this *is* the correct thing to do to keep
|
||||
* them in sync.
|
||||
*/
|
||||
for (auto it = plr->buyback.begin(); it != plr->buyback.end(); it++) {
|
||||
/*
|
||||
* XXX: we really need a standard item comparison function that
|
||||
* will work properly across all builds (ex. with iSerial)
|
||||
*/
|
||||
if (it->iType == item.iType && it->iID == item.iID && it->iOpt == item.iOpt
|
||||
&& it->iTimeLimit == item.iTimeLimit) {
|
||||
plr->buyback.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
//std::cout << (int)plr->buyback.size() << " items in buyback\n";
|
||||
|
||||
Items::Item* itemDat = Items::getItemData(item.iID, item.iType);
|
||||
|
||||
if (itemDat == nullptr) {
|
||||
std::cout << "[WARN] Item id " << item.iID << " with type " << item.iType << " not found (rebuy)" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// sell price is used on rebuy. ternary identifies stacked items
|
||||
int itemCost = itemDat->sellPrice * (itemDat->stackSize > 1 ? item.iOpt : 1);
|
||||
int slot = Items::findFreeSlot(plr);
|
||||
if (itemCost > plr->money || slot == -1) {
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot != req->iInvenSlotNum) {
|
||||
// possible item stacking?
|
||||
std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl;
|
||||
}
|
||||
|
||||
plr->money = plr->money - itemCost;
|
||||
plr->Inven[slot] = item;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp);
|
||||
// response parameters
|
||||
resp.iCandy = plr->money;
|
||||
resp.iInvenSlotNum = slot;
|
||||
resp.Item = item;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC);
|
||||
}
|
||||
|
||||
static void vendorTable(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE*)data->buf;
|
||||
|
||||
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end())
|
||||
return;
|
||||
|
||||
std::vector<VendorListing>& listings = Vendors::VendorTables[req->iVendorID];
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, resp);
|
||||
|
||||
for (int i = 0; i < (int)listings.size() && i < 20; i++) { // 20 is the max
|
||||
sItemBase base = {};
|
||||
base.iID = listings[i].id;
|
||||
base.iType = listings[i].type;
|
||||
|
||||
/*
|
||||
* Set vehicle expiry value.
|
||||
*
|
||||
* Note: sItemBase.iTimeLimit in the context of vendor listings contains
|
||||
* a duration, unlike in most other contexts where it contains the
|
||||
* expiration timestamp.
|
||||
*/
|
||||
if (listings[i].type == 10)
|
||||
base.iTimeLimit = VEHICLE_EXPIRY_DURATION;
|
||||
|
||||
sItemVendor vItem;
|
||||
vItem.item = base;
|
||||
vItem.iSortNum = listings[i].sort;
|
||||
vItem.iVendorID = req->iVendorID;
|
||||
//vItem.fBuyCost = listings[i].price; // this value is not actually the one that is used
|
||||
|
||||
resp.item[i] = vItem;
|
||||
}
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC);
|
||||
}
|
||||
|
||||
static void vendorStart(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_START*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_START_SUCC, resp);
|
||||
|
||||
resp.iNPC_ID = req->iNPC_ID;
|
||||
resp.iVendorID = req->iVendorID;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_START_SUCC);
|
||||
}
|
||||
|
||||
static void vendorBuyBattery(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_BATTERY_BUY*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int cost = req->Item.iOpt * 100;
|
||||
if ((req->Item.iID == 3 ? (plr->batteryW >= 9999) : (plr->batteryN >= 9999)) || plr->money < cost || req->Item.iOpt < 0) { // sanity check
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp);
|
||||
failResp.iErrorCode = 0;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
cost = plr->batteryW + plr->batteryN;
|
||||
plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 100 : 0;
|
||||
plr->batteryN += req->Item.iID == 4 ? req->Item.iOpt * 100 : 0;
|
||||
|
||||
// caps
|
||||
if (plr->batteryW > 9999)
|
||||
plr->batteryW = 9999;
|
||||
if (plr->batteryN > 9999)
|
||||
plr->batteryN = 9999;
|
||||
|
||||
cost = plr->batteryW + plr->batteryN - cost;
|
||||
plr->money -= cost;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp);
|
||||
|
||||
resp.iCandy = plr->money;
|
||||
resp.iBatteryW = plr->batteryW;
|
||||
resp.iBatteryN = plr->batteryN;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC);
|
||||
}
|
||||
|
||||
static void vendorCombineItems(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_ITEM_COMBINATION*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// prepare fail packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_FAIL, failResp);
|
||||
failResp.iCostumeItemSlot = req->iCostumeItemSlot;
|
||||
failResp.iStatItemSlot = req->iStatItemSlot;
|
||||
failResp.iErrorCode = 0;
|
||||
|
||||
// sanity check slot indices
|
||||
if (req->iCostumeItemSlot < 0 || req->iCostumeItemSlot >= AINVEN_COUNT || req->iStatItemSlot < 0 || req->iStatItemSlot >= AINVEN_COUNT) {
|
||||
std::cout << "[WARN] Inventory slot(s) out of range (" << req->iStatItemSlot << " and " << req->iCostumeItemSlot << ")" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
sItemBase* itemStats = &plr->Inven[req->iStatItemSlot];
|
||||
sItemBase* itemLooks = &plr->Inven[req->iCostumeItemSlot];
|
||||
Items::Item* itemStatsDat = Items::getItemData(itemStats->iID, itemStats->iType);
|
||||
Items::Item* itemLooksDat = Items::getItemData(itemLooks->iID, itemLooks->iType);
|
||||
|
||||
// sanity check item and combination entry existence
|
||||
if (itemStatsDat == nullptr || itemLooksDat == nullptr
|
||||
|| Items::CrocPotTable.find(abs(itemStatsDat->level - itemLooksDat->level)) == Items::CrocPotTable.end()) {
|
||||
std::cout << "[WARN] Either item ids or croc pot value set not found" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// sanity check matching item types
|
||||
if (itemStats->iType != itemLooks->iType
|
||||
|| (itemStats->iType == 0 && itemStatsDat->weaponType != itemLooksDat->weaponType)) {
|
||||
std::cout << "[WARN] Player attempted to combine mismatched items" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
CrocPotEntry* recipe = &Items::CrocPotTable[abs(itemStatsDat->level - itemLooksDat->level)];
|
||||
int cost = itemStatsDat->buyPrice * recipe->multStats + itemLooksDat->buyPrice * recipe->multLooks;
|
||||
float successChance = recipe->base / 100.0f; // base success chance
|
||||
|
||||
// rarity gap multiplier
|
||||
switch (abs(itemStatsDat->rarity - itemLooksDat->rarity)) {
|
||||
case 0:
|
||||
successChance *= recipe->rd0;
|
||||
break;
|
||||
case 1:
|
||||
successChance *= recipe->rd1;
|
||||
break;
|
||||
case 2:
|
||||
successChance *= recipe->rd2;
|
||||
break;
|
||||
case 3:
|
||||
successChance *= recipe->rd3;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
float rolled = Rand::randFloat(100.0f); // success chance out of 100
|
||||
//std::cout << rolled << " vs " << successChance << std::endl;
|
||||
plr->money -= cost;
|
||||
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, resp);
|
||||
if (rolled < successChance) {
|
||||
// success
|
||||
resp.iSuccessFlag = 1;
|
||||
|
||||
// modify the looks item with the new stats and set the appearance through iOpt
|
||||
itemLooks->iOpt = (int32_t)((itemLooks->iOpt) >> 16 > 0 ? (itemLooks->iOpt >> 16) : itemLooks->iID) << 16;
|
||||
itemLooks->iID = itemStats->iID;
|
||||
|
||||
// delete stats item
|
||||
itemStats->iID = 0;
|
||||
itemStats->iOpt = 0;
|
||||
itemStats->iTimeLimit = 0;
|
||||
itemStats->iType = 0;
|
||||
}
|
||||
else {
|
||||
// failure; don't do anything?
|
||||
resp.iSuccessFlag = 0;
|
||||
}
|
||||
resp.iCandy = plr->money;
|
||||
resp.iNewItemSlot = req->iCostumeItemSlot;
|
||||
resp.iStatItemSlot = req->iStatItemSlot;
|
||||
resp.sNewItem = *itemLooks;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_COMBINATION_SUCC);
|
||||
}
|
||||
|
||||
void Vendors::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_START, vendorStart);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE, vendorTable);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_BUY, vendorBuy);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_SELL, vendorSell);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY, vendorBuyback);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY, vendorBuyBattery);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_COMBINATION, vendorCombineItems);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user