mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2025-10-23 21:40:13 +00:00
Compare commits
1039 Commits
1.0
...
551d4ebb8b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
551d4ebb8b | ||
| c636c538eb | |||
| d3bef95a7f | |||
|
|
650f947451 | ||
|
|
613a4c58a3 | ||
|
|
177565dc55 | ||
|
|
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 | |||
| df655dfe29 | |||
| c8c4ec7d01 | |||
| 458843958b | |||
| 958d4a79eb | |||
| e86860baf7 | |||
|
|
a8c88a9bd9 | ||
|
|
038ce984c5 | ||
|
|
361c069d0c | ||
|
|
3876e0537e | ||
|
|
8a481acdae | ||
| e936cb9fac | |||
|
|
589da3f714 | ||
| 266ca8b8c6 | |||
| 00c5e07f4f | |||
| 05d035717d | |||
| f41bf0ace2 | |||
| 063d302bd5 | |||
| 7e9793bf90 | |||
|
|
fa6b0b178c | ||
|
|
dadf1c5bcf | ||
|
|
359991e274 | ||
|
|
260759c20b | ||
|
|
1ff5694960 | ||
|
|
be4c5a8072 | ||
|
|
ef84ec8fca | ||
|
|
11801c1f89 | ||
| 4f6c77be4f | |||
| 73c67a814d | |||
| d7a41d40ab | |||
| 0aeac0f6f3 | |||
| 48b0866441 | |||
| 4ade533f40 | |||
| 1e344c2dd8 | |||
| fdd0160248 | |||
| 5f10718315 | |||
|
|
da293ba9b3 | ||
|
|
437063d78a | ||
|
|
b239fb9331 | ||
| 50431024c9 | |||
| 2a258a80f0 | |||
| 322bc46604 | |||
| 2551f74af1 | |||
|
|
1af240fa34 | ||
| a067975f27 | |||
| 72a811d6ab | |||
| 3b35e0017a | |||
| 4df812f996 | |||
| 67d899efe6 | |||
| 64accecc30 | |||
| c8c2f4b05f | |||
| 3c43dd0193 | |||
| 9e9161083d | |||
|
|
5cf7225f52 | ||
|
|
5c8a0069fc | ||
| 64d4b1d26a | |||
| 9b0cb7f441 | |||
| c48db0f9f9 | |||
| e0c00bcdc8 | |||
| 6db1a7baf1 | |||
| 99e357d24e | |||
|
|
57681cd669 | ||
| c9badae526 | |||
|
|
d3ca93a9b8 | ||
|
|
6808365d48 | ||
| 4178945abe | |||
| 3e5101892b | |||
| 60be814e16 | |||
| 16c11dada0 | |||
| 260331715f | |||
| b187d4b65f | |||
| 3b3ddf08ef | |||
| 41898bb6b7 | |||
| dff710cf61 | |||
| b79bc56b31 | |||
| 9aa9b76826 | |||
| 113ce0af07 | |||
| 6f1a72ca0f | |||
| d964a83d6d | |||
| d025b611a1 | |||
| 2f1358c124 | |||
| c1b6ae8466 | |||
| 14bc368073 | |||
| c5dacb4958 | |||
| fb993f0c5d | |||
| 6d3868349d | |||
| 16bca39dae | |||
|
|
afbf309c7e | ||
| 28ad1a0c25 | |||
|
|
ff5f3966e3 | ||
| 55add82843 | |||
| e99feb03d5 | |||
| 431448ffb7 | |||
| 8105d0aa88 | |||
| acf358ef51 | |||
|
|
0a8e96ebc4 | ||
| 756074cc62 | |||
| 51a8cc8bdf | |||
| 651ccba932 | |||
| 1281fdaaf0 | |||
| 561a809f33 | |||
| 0d27412d81 | |||
|
|
1d792a21dd | ||
| d6b96389be | |||
|
|
6129c0b4e2 | ||
| c9bf3d1896 | |||
| 88953541ef | |||
| 94b0dc724e | |||
| 0ff1f74cd3 | |||
| 35b424c531 | |||
| 2072bdcff7 | |||
|
|
4f10ee0505 | ||
| cd9fb6ec25 | |||
|
|
56bf0db20d | ||
| 11fed7db10 | |||
| 35c622d8a2 | |||
| 43f2def80b | |||
| 0ac600e223 | |||
|
|
5f65a84b02 | ||
| 78c493b461 | |||
| f71e1349c1 | |||
| cff382a8ce | |||
| bbd6c5b532 | |||
|
|
ab6df26f92 | ||
|
|
786ee5f4f4 | ||
| c5efbceca3 | |||
| bf6c5d1b6b | |||
| fdfa7b5776 | |||
| f289c72f6f | |||
|
|
7318d9b578 | ||
|
|
caaffcbe3d | ||
| 3fe1a02200 | |||
| cd19c54824 | |||
|
|
88d08ffca7 | ||
|
|
7a46f061ed | ||
|
|
df18f3ccd1 | ||
| 1669ee3660 | |||
| 5d0b30b4cb | |||
|
|
0041da795a | ||
|
|
24be117e28 | ||
| c7f9358ae5 | |||
| eee1b52722 | |||
|
|
e3d53e8dcf | ||
|
|
b89df8d497 | ||
|
|
faf73fc835 | ||
|
|
aa2adcd9e2 | ||
|
|
e044b4251a | ||
| 7b085e9c8b | |||
|
|
da11220762 | ||
| 1425074ccb | |||
|
|
c66ac111ab | ||
|
|
32a37acd5a | ||
| 49fbdd2154 | |||
| 6857f50c30 | |||
|
|
c827b5a1b6 | ||
|
|
7f8e7dfa1c | ||
|
|
09b21c54d3 | ||
|
|
c549192f59 | ||
| 6843faeb8d | |||
| b43628a19d | |||
| b94f602537 | |||
|
|
fa5f194cc7 | ||
| fbc3c79aa2 | |||
| 93d973bf21 | |||
| 7cf239e3af | |||
|
|
14d556976d | ||
| af6158fbb2 | |||
| c0abab39ae | |||
| dff6e8c23b | |||
| d6e1f57c23 | |||
| 9f3f9bb9c3 | |||
| 0654500df3 | |||
| a0065c2050 | |||
|
|
795107a274 | ||
| 42597c2a7a | |||
|
|
bb1ce5c28d | ||
|
|
e75049fc98 | ||
|
|
1ec4634f69 | ||
| 4d9072a752 | |||
| a0d59419f1 | |||
| 94cb89dd6b | |||
| ba81db97ef | |||
| ec84d6ca58 | |||
| b8f7d2efc6 | |||
| e7b58c4b32 | |||
| 0f5be27c97 | |||
| 8328ebf4f3 | |||
| a17b72b0b3 | |||
| 95e454232a | |||
| fafde9348e | |||
| f2059c9ce1 | |||
|
|
e5274045b0 | ||
|
|
941d98cc07 | ||
|
|
3a892de7c7 | ||
| 8e87a3f102 | |||
| 24d30a05bf | |||
| b2325eb308 | |||
| 47b76b422c | |||
| 208f4b3bbd | |||
| 78e096a411 | |||
| 2e9e265cba | |||
| 8400a15262 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
version.h
|
||||
23
.editorconfig
Normal file
23
.editorconfig
Normal file
@@ -0,0 +1,23 @@
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# 4 space indentation
|
||||
[*.cpp,*.hpp]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Tabs in makefile
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Don't enforce anything in vendored code
|
||||
[/vendor/**]
|
||||
end_of_line = unset
|
||||
insert_final_newline = unset
|
||||
indent_style = unset
|
||||
indent_style = unset
|
||||
5
.gitattributes
vendored
Normal file
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
|
||||
146
.github/workflows/check-builds.yaml
vendored
Normal file
146
.github/workflows/check-builds.yaml
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
name: Check Builds
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- src/**
|
||||
- vendor/**
|
||||
- .github/workflows/check-builds.yaml
|
||||
- CMakeLists.txt
|
||||
- Makefile
|
||||
pull_request:
|
||||
types: ready_for_review
|
||||
paths:
|
||||
- src/**
|
||||
- vendor/**
|
||||
- CMakeLists.txt
|
||||
- Makefile
|
||||
|
||||
jobs:
|
||||
ubuntu-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set environment
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
run: sudo apt install clang cmake snap -y && sudo snap install powershell --classic
|
||||
- name: Check compilation
|
||||
run: |
|
||||
$versions = "104", "728", "1013"
|
||||
|
||||
foreach ($version in $versions) {
|
||||
Write-Output "Cleaning old output"
|
||||
Invoke-Expression "make clean"
|
||||
if ($LASTEXITCODE -ne "0") {
|
||||
Write-Error "make clean failed for version $version" -ErrorAction Stop
|
||||
}
|
||||
Write-Output "Building version $version"
|
||||
Invoke-Expression "make -j8 PROTOCOL_VERSION=$version"
|
||||
if ($LASTEXITCODE -ne "0") {
|
||||
Write-Error "make failed for version $version" -ErrorAction Stop
|
||||
}
|
||||
Rename-Item -Path "bin/fusion" -newName "$version-fusion"
|
||||
Write-Output "Built version $version"
|
||||
}
|
||||
Copy-Item -Path "sql" -Destination "bin/sql" -Recurse
|
||||
Copy-Item -Path "config.ini" -Destination "bin"
|
||||
shell: pwsh
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'ubuntu20_04-bin-x64-${{ env.SHORT_SHA }}'
|
||||
path: bin
|
||||
|
||||
windows-build:
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- name: Set environment
|
||||
run: $s = $env:GITHUB_SHA.subString(0, 7); echo "SHORT_SHA=$s" >> $env:GITHUB_ENV
|
||||
shell: pwsh
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Check compilation
|
||||
run: |
|
||||
$versions = "104", "728", "1013"
|
||||
$configurations = "Release"
|
||||
# "Debug" builds are disabled, since we don't really need them
|
||||
|
||||
$vsPath = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise"
|
||||
|
||||
Import-Module "$vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
|
||||
Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation
|
||||
|
||||
Invoke-Expression "vcpkg install sqlite3:x64-windows"
|
||||
Invoke-Expression "vcpkg integrate install"
|
||||
|
||||
foreach ($version in $versions) {
|
||||
if (Test-Path -LiteralPath "build") {
|
||||
Remove-Item "build" -Recurse
|
||||
Write-Output "Deleted existing build folder"
|
||||
}
|
||||
Invoke-Expression "cmake -B build -DPROTOCOL_VERSION=$version -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake"
|
||||
if ($LASTEXITCODE -ne "0") {
|
||||
Write-Error "cmake generation failed for version $version" -ErrorAction Stop
|
||||
}
|
||||
Write-Output "Generated build files for version $version"
|
||||
|
||||
foreach ($configuration in $configurations) {
|
||||
Write-Output "Building version $version $configuration"
|
||||
Invoke-Expression "msbuild build\OpenFusion.sln /maxcpucount:8 /p:BuildInParallel=true /p:CL_MPCount=8 /p:UseMultiToolTask=true /p:Configuration=$configuration"
|
||||
if ($LASTEXITCODE -ne "0") {
|
||||
Write-Error "msbuild build failed for version $version" -ErrorAction Stop
|
||||
}
|
||||
Rename-Item -Path "bin/$configuration" -newName "$version-$configuration"
|
||||
Write-Output "Built version $version $configuration"
|
||||
Copy-Item -Path "sql" -Destination "bin/$version-$configuration/sql" -Recurse
|
||||
Copy-Item -Path "config.ini" -Destination "bin/$version-$configuration"
|
||||
}
|
||||
}
|
||||
shell: pwsh
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'windows-vs2019-bin-x64-${{ env.SHORT_SHA }}'
|
||||
path: bin
|
||||
|
||||
copy-artifacts:
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [windows-build, ubuntu-build]
|
||||
env:
|
||||
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}
|
||||
ENDPOINT: ${{ secrets.ENDPOINT }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- run: |
|
||||
GITDESC=$(git describe --tags)
|
||||
mkdir $GITDESC
|
||||
echo "ARTDIR=$GITDESC" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: ${{ env.ARTDIR }}
|
||||
- name: Upload artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt install zip -y
|
||||
cd $ARTDIR
|
||||
for build in *; do
|
||||
cd $build
|
||||
zip -r ../$build.zip *
|
||||
cd ..
|
||||
rm -r $build
|
||||
done
|
||||
cd ..
|
||||
umask 077
|
||||
printf %s "$BOT_SSH_KEY" > cdn_key
|
||||
scp -i cdn_key -o StrictHostKeyChecking=no -r $ARTDIR $ENDPOINT
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,4 +1,18 @@
|
||||
.vscode
|
||||
.vscode/
|
||||
bin/*
|
||||
notes.txt
|
||||
config.ini
|
||||
config.ini
|
||||
*.o
|
||||
tags
|
||||
*~
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
build/
|
||||
.vs/
|
||||
.idea/
|
||||
*.db
|
||||
*-shm
|
||||
*-wal
|
||||
version.h
|
||||
infer-out
|
||||
gmon.out
|
||||
|
||||
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
|
||||
9
.vimrc
Normal file
9
.vimrc
Normal file
@@ -0,0 +1,9 @@
|
||||
" vim configuration file
|
||||
|
||||
" You will need to put 'set exrc' and 'set secure' into your main .vimrc file,
|
||||
" in which case this file will be loaded automatically, but *only* if you
|
||||
" start vim in this dir. Alternatively you can just load it directly with
|
||||
" ':so .vimrc' every time.
|
||||
set tabstop=4
|
||||
set shiftwidth=4
|
||||
set expandtab
|
||||
26
.vscode/launch.json
vendored
Normal file
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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
60
CMakeLists.txt
Normal file
60
CMakeLists.txt
Normal file
@@ -0,0 +1,60 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(OpenFusion)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
# OpenFusion supports multiple packet/struct versions
|
||||
# 104 is the default version to build which can be changed
|
||||
# For example: cmake -B build -DPROTOCOL_VERSION=728
|
||||
set(PROTOCOL_VERSION 104 CACHE STRING "The packet version to build")
|
||||
|
||||
add_compile_definitions(PROTOCOL_VERSION=${PROTOCOL_VERSION})
|
||||
|
||||
# Disallow in-source builds
|
||||
if (${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
|
||||
message(FATAL_ERROR "In-source builds not allowed. Please refer to the wiki for more information. Please remove the CMakeFiles folder and the CMakeCache.txt file.")
|
||||
endif()
|
||||
|
||||
# Output binaries to the bin folder in the source directory
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
|
||||
|
||||
# Put CMake targets (ALL_BUILD/ZERO_CHECK) into a folder
|
||||
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
|
||||
|
||||
# Set the OpenFusion project as the default startup project for VS
|
||||
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT openfusion)
|
||||
|
||||
if (WIN32)
|
||||
# Set the output binary name to winfusion to match the regular Makefile
|
||||
set(BIN_NAME winfusion)
|
||||
else()
|
||||
set(BIN_NAME fusion)
|
||||
endif()
|
||||
|
||||
include_directories(src vendor)
|
||||
|
||||
file(GLOB_RECURSE SOURCES src/**.[ch]pp vendor/**.[ch]pp vendor/**.[ch] version.h)
|
||||
|
||||
configure_file(version.h.in ${CMAKE_SOURCE_DIR}/version.h @ONLY)
|
||||
|
||||
add_executable(openfusion ${SOURCES})
|
||||
|
||||
set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME})
|
||||
|
||||
# find sqlite3 and use it
|
||||
find_package(SQLite3 REQUIRED)
|
||||
target_include_directories(openfusion PRIVATE ${SQLite3_INCLUDE_DIRS})
|
||||
target_link_libraries(openfusion PRIVATE ${SQLite3_LIBRARIES})
|
||||
|
||||
# Makes it so config, tdata, etc. get picked up when starting via the debugger in VS
|
||||
set_property(TARGET openfusion PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# Use pthreads if not generating a VS solution or MinGW makefile (because MinGW will prefer Win32 threads)
|
||||
# Checking if the compiler ID is MSVC will allow us to open the project as a CMake project in VS.
|
||||
# It's not something you should do, but it's there if you need it...
|
||||
if (NOT CMAKE_GENERATOR MATCHES "Visual Studio" AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND NOT CMAKE_GENERATOR MATCHES "MinGW Makefiles")
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(openfusion PRIVATE pthread)
|
||||
endif()
|
||||
83
CONTRIBUTING.md
Normal file
83
CONTRIBUTING.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Contributing
|
||||
|
||||
If you want to contribute to OpenFusion's development, that's great!
|
||||
We'd appreciate it if you already had some programming experience, as well as experience with software version control using git.
|
||||
|
||||
If you've never used git before, please take the time to research it yourself.
|
||||
There is an abundance of online resources for getting started with git.
|
||||
|
||||
If, however, you have no experience programming, this type of project may not be the best one to start with.
|
||||
OpenFusion is written in C++, which really isn't the best language to start with, as it has many extremely subtle pitfalls with potentially catastrophic consequences that are often very hard for an inexperienced programmer to detect and avoid.
|
||||
This is compounded by the fact that this is a server, which means it's a long-running, multithreaded process exposed to the network, decoding an externally-imposed binary protocol -- written in C++.
|
||||
A bad combination.
|
||||
|
||||
With that out of the way, the rest of this document will serve to address the matter of clean commits in Pull Requests.
|
||||
|
||||
## Repository cleanliness
|
||||
|
||||
The commit history in a git repository is important.
|
||||
Unlike some other version control systems, git allows developers to destructively edit history so as to enjoy the benefits of both frequent "saves" (as in, having the ability to record corrections as soon as you make them) and clean commits which perfectly encapsulate a given change.
|
||||
Each developer has their own preferred way of doing things, of course, but in general one commit should represent one functional change in the codebase as a whole.
|
||||
Developers should be able to view a project's commit history and easily understand how the project has evolved over time.
|
||||
|
||||
Achieving this is often daunting for new users of git.
|
||||
I know of two intermediate-level resources (that is, for after you've learned the basics of git) I can recommend for understanding how to properly manage your repository's history -- [think-like-a-git.net](http://think-like-a-git.net/) and [git-rebase.io](https://git-rebase.io/).
|
||||
Both are pretty short reads and following them will get you up to speed with branches and the rebasing thereof.
|
||||
|
||||
I will now cover a few examples of the complications people have encountered contributing to this project, how to understand them, overcome them and henceforth avoid them.
|
||||
|
||||
## Dirty pull requests
|
||||
|
||||
Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge.
|
||||
These are generally either:
|
||||
|
||||
* Dozens of quick-fix commits the author made while working on their contribution
|
||||
* Countless useless merge commits generated while trying to re-synchronize with the upstream repository
|
||||
|
||||
Few developers are fine with having their commit histories utterly destroyed by merging these changes in.
|
||||
Many projects, when presented with such Pull Requests, will flat-out reject them or demand the author clean them up first before they can be accepted.
|
||||
|
||||
Cpunch, however, chooses to accept them anyway, but squashes them into the repository with the "rebase" merge strategy, instead of a regular merge.
|
||||
Whereas a regular merge creates a "merge commit" which unites two branches together, a rebase instead *reconstructs* the commits from one branch onto the other, creating *different commits* with possibly the same contents.
|
||||
|
||||
If you read the above links, you'll note that this isn't exactly a perfect solution either, since rewriting history in public is usually a bad idea, but because these changes were originally only visible to the PR author, it is only they that will need to rebase their fork to re-sync with the upstream repository.
|
||||
The obvious issue, then, is that the people submitting dirty PRs are the exact people who don't *know* how to rebase their fork to continue submitting their work cleanly.
|
||||
So they end up creating countless merge commits when pulling upstream on top of their own incompatible histories, and then submitting those merge commits in their PRs and the cycle continues.
|
||||
|
||||
## The details
|
||||
|
||||
A git commit is uniquely identified by its SHA1 hash.
|
||||
Its hash is generated from its contents, as well as the hash(es) of its parent commit(s).
|
||||
(Most commits have one parent. Merge commits almost always have two, but octopus merges can have any number of parents.)
|
||||
That means that even if two commits are exactly the same in terms of content, they're not the same commit if their parent commits differ (ex. if they've been rebased).
|
||||
So if you keep issuing `git pull`s after upstream has merged a rebased version of your PR, you will never re-synchronize with it, and will instead construct an alternate history polluted by pointless merge commits.
|
||||
|
||||
## The solution
|
||||
|
||||
If you already have a messed-up fork and you have no changes on it that you're afraid to lose, the solution is simple:
|
||||
|
||||
* Ensure your `upstream` remote is up to date with `git fetch upstream`
|
||||
* Make sure you're on master (`git branch master`)
|
||||
* Set your master branch to the latest commit with `git reset --hard upstream/master`
|
||||
* Propagate the change to your GitHub fork with `git push -f origin master`
|
||||
|
||||
And you're good to go.
|
||||
If you do have some committed changes that haven't yet been merged upstream, you should probably save them on another branch (called "backup" or something) with `git checkout -b backup`.
|
||||
|
||||
If you do end up messing something up, don't worry, it most likely isn't really lost and `git reflog` is your friend.
|
||||
(You can checkout an arbitrary commit, and make it into its own branch with `git checkout -b BRANCH` or set a pre-exisitng branch to it with `git reset --hard COMMIT`)
|
||||
|
||||
## Avoiding the problem
|
||||
|
||||
When working on a changeset you want to submit back upstream, don't do it on the main branch.
|
||||
Create a work branch just for your changeset with `git checkout -b work`.
|
||||
That way you can always keep master in sync with upstream with `git pull --ff-only upstream master`.
|
||||
(`--ff-only` can be left out. If you find that git want you to make a merge commit, just back out of it by saving an empty commit message, then fix whatever the cause was.)
|
||||
|
||||
* If upstream gets new changes before you've had a chance to submit yours, just update master, create a new branch from your work branch and rebase your new work branch on master (such that your up-to-date changeset is now on the new work branch) and submit that one for your PR
|
||||
* If you end up making a few ugly fixup commits, use `git rebase --interactive` to clean them up (on a new branch) before submitting your changeset
|
||||
* If you get told to change something in the PR before it's merged, but after you've pushed it to your GitHub fork, rebase your changes locally, then force-push them onto the your fork's PR branch with `git push -f origin work1` or so
|
||||
|
||||
Creating new branches for the rebase isn't strictly necessary since you can always return a branch to its previous state with `git reflog`.
|
||||
|
||||
For moving uncommited changes around between branches, `git stash` is a real blessing.
|
||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM debian:latest
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get -y update && apt-get install -y \
|
||||
git \
|
||||
clang \
|
||||
make \
|
||||
libsqlite3-dev
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN make -j8
|
||||
|
||||
# tabledata should be copied from the host;
|
||||
# clone it there before building the container
|
||||
#RUN git submodule update --init --recursive
|
||||
|
||||
CMD ["./bin/fusion"]
|
||||
|
||||
LABEL Name=openfusion Version=0.0.1
|
||||
22
LICENSE.md
22
LICENSE.md
@@ -1,9 +1,21 @@
|
||||
The OpenFusion MIT except Marlorn License
|
||||
MIT License
|
||||
|
||||
Copyright 2020 Seth Stubbs
|
||||
Copyright (c) 2020-2023 OpenFusion Contributors
|
||||
|
||||
Excluding the individual known as "MarlornWS" and their associates, permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
186
Makefile
186
Makefile
@@ -1,23 +1,175 @@
|
||||
# makefile for OpenFusion
|
||||
|
||||
OBJS = src/*.cpp # source files to compile
|
||||
CC = clang++ # using GNU C++ compiler
|
||||
WIN_CC = x86_64-w64-mingw32-g++ # using GNU C++ compiler
|
||||
GIT_VERSION!=git describe --tags
|
||||
|
||||
CC=clang
|
||||
CXX=clang++
|
||||
# -w suppresses all warnings (the part that's commented out helps me find memory leaks, it ruins performance though!)
|
||||
COMPILER_FLAGS = -std=c++17 -o3 -static #-g3 -fsanitize=address
|
||||
WIN_COMPILER_FLAGS = -std=c++17 -o3 -static #-g3 -fsanitize=address
|
||||
# If compiling with ASAN, invoke like this: $ LSAN_OPTIONS=suppressions=suppr.txt bin/fusion
|
||||
CFLAGS=-Wall -Wno-unknown-pragmas -O2 -fPIE -D_FORTIFY_SOURCE=1 -fstack-protector #-g3 -fsanitize=address
|
||||
CXXFLAGS=$(CFLAGS) -std=c++17 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor
|
||||
LDFLAGS=-lpthread -lsqlite3 -pie -Wl,-z,relro -Wl,-z,now #-g3 -fsanitize=address
|
||||
# specifies the name of our exectuable
|
||||
SERVER=bin/fusion
|
||||
|
||||
#LINKER_FLAGS specifies the libraries we're linking against (NONE, this is a single header library.)
|
||||
LINKER_FLAGS = -lpthread
|
||||
WIN_LINKER_FLAGS = -lws2_32 -lwsock32
|
||||
# assign protocol version
|
||||
# this can be overriden by ex. make PROTOCOL_VERSION=728
|
||||
PROTOCOL_VERSION?=104
|
||||
|
||||
#OBJ_NAME specifies the name of our exectuable
|
||||
OBJ_NAME = bin/fusion # location of output for build
|
||||
WIN_OBJ_NAME = bin/winfusion.exe # location of output for build
|
||||
# Windows-specific
|
||||
WIN_CC=x86_64-w64-mingw32-gcc
|
||||
WIN_CXX=x86_64-w64-mingw32-g++
|
||||
WIN_CFLAGS=-O2 -D_WIN32_WINNT=0x0601 -Wall -Wno-unknown-pragmas
|
||||
WIN_CXXFLAGS=$(WIN_CFLAGS) -std=c++17 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor
|
||||
WIN_LDFLAGS=-static -lws2_32 -lwsock32 -lsqlite3
|
||||
WIN_SERVER=bin/winfusion.exe
|
||||
|
||||
all: $(OBJS)
|
||||
$(CC) $(OBJS) $(COMPILER_FLAGS) $(LINKER_FLAGS) -o $(OBJ_NAME)
|
||||
# C code; currently exclusively from vendored libraries
|
||||
CSRC=\
|
||||
vendor/bcrypt/bcrypt.c\
|
||||
vendor/bcrypt/crypt_blowfish.c\
|
||||
vendor/bcrypt/crypt_gensalt.c\
|
||||
vendor/bcrypt/wrapper.c\
|
||||
|
||||
windows: $(OBJS)
|
||||
$(WIN_CC) $(OBJS) $(WIN_COMPILER_FLAGS) $(WIN_LINKER_FLAGS) -o $(WIN_OBJ_NAME)
|
||||
CHDR=\
|
||||
vendor/bcrypt/bcrypt.h\
|
||||
vendor/bcrypt/crypt_blowfish.h\
|
||||
vendor/bcrypt/crypt_gensalt.h\
|
||||
vendor/bcrypt/ow-crypt.h\
|
||||
vendor/bcrypt/winbcrypt.h\
|
||||
|
||||
CXXSRC=\
|
||||
src/core/CNProtocol.cpp\
|
||||
src/core/CNShared.cpp\
|
||||
src/core/Packets.cpp\
|
||||
src/servers/CNLoginServer.cpp\
|
||||
src/servers/CNShardServer.cpp\
|
||||
src/servers/Monitor.cpp\
|
||||
src/db/init.cpp\
|
||||
src/db/login.cpp\
|
||||
src/db/shard.cpp\
|
||||
src/db/player.cpp\
|
||||
src/db/email.cpp\
|
||||
src/sandbox/seccomp.cpp\
|
||||
src/sandbox/openbsd.cpp\
|
||||
src/Chat.cpp\
|
||||
src/CustomCommands.cpp\
|
||||
src/Entities.cpp\
|
||||
src/Email.cpp\
|
||||
src/Eggs.cpp\
|
||||
src/main.cpp\
|
||||
src/Missions.cpp\
|
||||
src/MobAI.cpp\
|
||||
src/Combat.cpp\
|
||||
src/Nanos.cpp\
|
||||
src/Abilities.cpp\
|
||||
src/Items.cpp\
|
||||
src/NPCManager.cpp\
|
||||
src/PlayerManager.cpp\
|
||||
src/PlayerMovement.cpp\
|
||||
src/BuiltinCommands.cpp\
|
||||
src/settings.cpp\
|
||||
src/Transport.cpp\
|
||||
src/TableData.cpp\
|
||||
src/Chunking.cpp\
|
||||
src/Buddies.cpp\
|
||||
src/Groups.cpp\
|
||||
src/Racing.cpp\
|
||||
src/Vendors.cpp\
|
||||
src/Trading.cpp\
|
||||
src/Rand.cpp\
|
||||
|
||||
# headers (for timestamp purposes)
|
||||
CXXHDR=\
|
||||
src/core/CNProtocol.hpp\
|
||||
src/core/CNShared.hpp\
|
||||
src/core/CNStructs.hpp\
|
||||
src/core/Packets.hpp\
|
||||
src/core/Defines.hpp\
|
||||
src/core/Core.hpp\
|
||||
src/servers/CNLoginServer.hpp\
|
||||
src/servers/CNShardServer.hpp\
|
||||
src/servers/Monitor.hpp\
|
||||
src/db/Database.hpp\
|
||||
src/db/internal.hpp\
|
||||
src/sandbox/Sandbox.hpp\
|
||||
vendor/bcrypt/BCrypt.hpp\
|
||||
vendor/INIReader.hpp\
|
||||
vendor/JSON.hpp\
|
||||
vendor/INIReader.hpp\
|
||||
vendor/JSON.hpp\
|
||||
src/Chat.hpp\
|
||||
src/CustomCommands.hpp\
|
||||
src/Entities.hpp\
|
||||
src/Email.hpp\
|
||||
src/Eggs.hpp\
|
||||
src/Missions.hpp\
|
||||
src/MobAI.hpp\
|
||||
src/Combat.hpp\
|
||||
src/Nanos.hpp\
|
||||
src/Abilities.hpp\
|
||||
src/Items.hpp\
|
||||
src/NPCManager.hpp\
|
||||
src/Player.hpp\
|
||||
src/PlayerManager.hpp\
|
||||
src/PlayerMovement.hpp\
|
||||
src/BuiltinCommands.hpp\
|
||||
src/settings.hpp\
|
||||
src/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)
|
||||
|
||||
OBJ=$(COBJ) $(CXXOBJ)
|
||||
|
||||
HDR=$(CHDR) $(CXXHDR)
|
||||
|
||||
all: $(SERVER)
|
||||
|
||||
windows: $(SERVER)
|
||||
|
||||
# assign Windows-specific values if targeting Windows
|
||||
windows : CC=$(WIN_CC)
|
||||
windows : CXX=$(WIN_CXX)
|
||||
windows : CFLAGS=$(WIN_CFLAGS)
|
||||
windows : CXXFLAGS=$(WIN_CXXFLAGS)
|
||||
windows : LDFLAGS=$(WIN_LDFLAGS)
|
||||
windows : SERVER=$(WIN_SERVER)
|
||||
|
||||
.SUFFIXES: .o .c .cpp .h .hpp
|
||||
|
||||
.c.o:
|
||||
$(CC) -c $(CFLAGS) -o $@ $<
|
||||
|
||||
.cpp.o:
|
||||
$(CXX) -c $(CXXFLAGS) -o $@ $<
|
||||
|
||||
# header timestamps are a prerequisite for OF object files
|
||||
$(CXXOBJ): $(HDR)
|
||||
|
||||
$(SERVER): $(OBJ) $(CHDR) $(CXXHDR)
|
||||
mkdir -p bin
|
||||
$(CXX) $(OBJ) $(LDFLAGS) -o $(SERVER)
|
||||
|
||||
# compatibility with how cmake injects GIT_VERSION
|
||||
version.h:
|
||||
touch version.h
|
||||
|
||||
src/main.o: version.h
|
||||
|
||||
.PHONY: all windows clean nuke
|
||||
|
||||
# only gets rid of OpenFusion objects, so we don't need to
|
||||
# recompile the libs every time
|
||||
clean:
|
||||
rm -f src/*.o src/*/*.o $(SERVER) $(WIN_SERVER) version.h
|
||||
|
||||
# gets rid of all compiled objects, including the libraries
|
||||
nuke:
|
||||
rm -f $(OBJ) $(SERVER) $(WIN_SERVER) version.h
|
||||
|
||||
104
README.md
104
README.md
@@ -1,23 +1,43 @@
|
||||
# OpenFusion
|
||||
<p align="center"><img width="640" src="res/openfusion-hero.png" alt=""></p>
|
||||
|
||||
OpenFusion is a landwalker server for FusionFall. It currently supports versions `beta-20100104` and `beta-20100728` of the original game.
|
||||
<p align="center">
|
||||
<a href="https://github.com/OpenFusionProject/OpenFusion/releases/latest"><img src="https://img.shields.io/github/v/release/OpenFusionProject/OpenFusion" alt="Current Release"></a>
|
||||
<a href="https://github.com/OpenFusionProject/OpenFusion/actions/workflows/check-builds.yaml"><img src="https://github.com/OpenFusionProject/OpenFusion/actions/workflows/check-builds.yaml/badge.svg" alt="Workflow"></a>
|
||||
<a href="https://discord.gg/DYavckB"><img src="https://img.shields.io/badge/chat-on%20discord-7289da.svg?logo=discord" alt="Discord"></a>
|
||||
<a href="https://github.com/OpenFusionProject/OpenFusion/blob/master/LICENSE.md"><img src="https://img.shields.io/github/license/OpenFusionProject/OpenFusion" alt="License"></a>
|
||||
</p>
|
||||
|
||||
Further documentation pending.
|
||||
OpenFusion is a reverse-engineered server for FusionFall. It primarily targets versions `beta-20100104` and `beta-20111013` of the original game, with [limited support](https://github.com/OpenFusionProject/OpenFusion/wiki/FusionFall-Version-Support) for others.
|
||||
|
||||
## Usage
|
||||
|
||||
tl;dr:
|
||||
### Getting Started
|
||||
#### Method A: Installer (Easiest)
|
||||
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4-Installer.exe) - choose to run the file.
|
||||
2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
|
||||
3. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
|
||||
4. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 3.
|
||||
|
||||
1. Download the client+server bundle from [here](...).
|
||||
2. Run `FreeClient/installUnity.bat` once
|
||||
#### Method B: Standalone .zip file
|
||||
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4.zip).
|
||||
2. Extract it to a folder of your choice. Note: if you are upgrading from an older version, it is preferable to start with a fresh folder rather than overwriting a previous install.
|
||||
3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
|
||||
4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
|
||||
5. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 4.
|
||||
|
||||
From then on, any time you want to run the "game":
|
||||
Instructions for getting the client to run on Linux through Wine can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Running-the-game-client-on-Linux).
|
||||
|
||||
3. Run `OpenFusion/winfusion.exe`
|
||||
4. Run `FreeClient/OpenFusionClient.exe`
|
||||
### Hosting a server
|
||||
|
||||
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.
|
||||
1. Grab `OpenFusionServer-1.4-original.zip` or `OpenFusionServer-1.4-academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.4).
|
||||
2. Extract it to a folder of your choice, then run `winfusion.exe` (Windows) or `fusion` (Linux) to start the server.
|
||||
3. Add a new server to the client's list:
|
||||
1. For Description, enter anything you want. This is what will show up in the server list.
|
||||
2. For Server IP, enter the IP address and port of the login server. If you're hosting and playing on the same PC, this would be `127.0.0.1:23000`.
|
||||
3. Lastly Game Version - select `beta-20100104` if you downloaded the original zip, or `beta-20111013` if you downloaded the academy zip.
|
||||
5. Once you've added the server to the list, connect to it and log in. If you're having trouble with this, refer to steps 4 and 5 from the previous section.
|
||||
|
||||
If you want 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.
|
||||
|
||||
@@ -36,7 +56,7 @@ The original game made use of the player's actual web browser to launch the game
|
||||
|
||||
The browser/Electron client opens a web page with an `<embed>` tag of MIME type `application/vnd.unity`, where the `src` param is the address of the game's `.unity3d` entrypoint.
|
||||
|
||||
This triggers the browser to load an NPAPI plugin that handles this MIME type, the Unity Web Player, which the browser looks for in `C:\Users\USERNAME\AppData\LocalLow\Unity\WebPlayer`.
|
||||
This triggers the browser to load an NPAPI plugin that handles this MIME type, the Unity Web Player, which the browser looks for in `C:\Users\%USERNAME%\AppData\LocalLow\Unity\WebPlayer`.
|
||||
The Web Player was previously copied there by `installUnity.bat`.
|
||||
|
||||
Note that the version of the web player distributed with OpenFusion expects a standard `UnityWeb` magic number for all assets, instead of Retro's modified `streamed` magic number.
|
||||
@@ -44,44 +64,64 @@ This will potentially become relevant later, as people start experimenting and m
|
||||
|
||||
The web player will execute the game code, which will request the following files from the server: `/assetInfo.php` and `/loginInfo.php`.
|
||||
|
||||
`FreeClient/resources/app/files/assetInfo.php` contains the address from which to fetch the rest of the game's assets (the "dongresources").
|
||||
`/assetInfo.php` contains the address from which to fetch the rest of the game's assets (the "dongresources").
|
||||
Normally those would be hosted on the same web server as the gateway, but the OpenFusion distribution (in it's default configuration) doesn't use a web server at all!
|
||||
It loads the web pages locally using the `file://` schema, and fetches the game's assets from Turner's CDN (which is still hosting them to this day!).
|
||||
|
||||
`FreeClient/resources/app/files/loginInfo.php` contains the IP:port pair of the FusionFall login server, which the client will connect to. This login server drives the client while it's in the Character Selection menu, as well as Character Creation and the Tutorial.
|
||||
`/loginInfo.php` contains the IP:port pair of the FusionFall login server, which the client will connect to. This login server drives the client while it's in the Character Selection menu, as well as Character Creation and the Tutorial.
|
||||
|
||||
When the player clicks "ENTER THE GAME" (or completes the tutorial), the login server sends it the address of the shard server, which the client will then connect to and remain connected to during gameplay.
|
||||
|
||||
## Configuration
|
||||
|
||||
You can change the ports the FusionFall server listens on in `OpenFusion/config.ini`. Make sure the login server port is in sync with `loginInfo.php`.
|
||||
The shard port needs no such synchronization.
|
||||
You can also configure the distance at which you'll be able to see other players, though by default it's already as high as you'll want it.
|
||||
You can change the ports the FusionFall server listens on in `config.ini`. Make sure the login server port is in sync with what you enter into the client's server list - the shard port needs no such synchronization.
|
||||
|
||||
If you want to play with friends, you can change the IP in `loginInfo.php` to a login server hosted elsewhere.
|
||||
This config file also has several other options you can tweak, including log verbosity, database saving interval, default account/permission level, and more. See the comments within [the config file itself](https://github.com/OpenFusionProject/OpenFusion/blob/master/config.ini) for more details.
|
||||
|
||||
If you want to play with friends, simply enter the login server details into the `Add Server` dialogue in OpenFusionClient.
|
||||
This just works if you're all under the same LAN, but if you want to play over the internet you'll need to open a port, use a service like Hamachi or nGrok, or host the server on a VPS (just like any other gameserver).
|
||||
|
||||
If you're in a region in which Turner's CDN doesn't still have the game's assets cached, you won't be able to play the game in its default configuration.
|
||||
You'll need to obtain the necessary assets elsewhere and set up your own local web server to host them, because unlike web browsers, the game itself cannot interpret the `file://` schema, and will thus need the assets hosted on an actual HTTP server.
|
||||
Don't forget to point `assetInfo.php` to where you're hosting the assets and change the `src` param of both the `<embed>` tag and the `<object>` tag in `FreeClient/resources/files/index.html` to where you're hosting the `.unity3d` entrypoint.
|
||||
|
||||
If you change `loginInfo.php` or `assetInfo.php`, make sure not to put any newline characters (or any other whitespace) at the end of the file(s).
|
||||
Some modern IDEs/text editors do this automatically. If all else fails, use Notepad.
|
||||
|
||||
## Compiling
|
||||
|
||||
OpenFusion can be compiled from source using the included makefile. to compile for windows (on a *nix system) use `make windows`, otherwise to compile it for the current platform you're on just run `make`
|
||||
OpenFusion has one external dependency: SQLite. The oldest compatible version is `3.33.0`. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [read this guide on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/Installing-SQLite-on-Windows-using-vcpkg).
|
||||
|
||||
## "Gameplay"
|
||||
You have two choices for compiling OpenFusion: the included Makefile and the included CMakeLists file.
|
||||
|
||||
Notice the quotes. This is not a full-fledged game that can be played.
|
||||
It's what's called a landwalker; enough of the server has been implemented to allow players to run around in the game world, and not much else.
|
||||
### Makefile
|
||||
|
||||
A detailed compilation guide is available for Windows users in the wiki [using MinGW-w64 and MSYS2](https://github.com/OpenFusionProject/OpenFusion/wiki/Compilation-on-Windows). Otherwise, to compile it for the current platform you're on, just run `make` with the correct build tools installed (currently make and clang).
|
||||
|
||||
### CMake
|
||||
|
||||
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
|
||||
|
||||
The goal of the project is to faithfully recreate the game as it was at the time of the targeted build.
|
||||
The server is not yet complete, however, and some functionality is still missing.
|
||||
|
||||
Because the server is still in development, ordinary players are allowed access to a few admin commands:
|
||||
|
||||

|
||||
|
||||
To make your landwalking experience more pleasant, you can make use of a few admin commands to get around easier:
|
||||
|
||||
### Movement commands
|
||||
* A `/speed` of around 2400 or 3000 is nice.
|
||||
* A `/jump` of about 50 will send you soaring
|
||||
* [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.).
|
||||
* `/goto` is useful for more precise teleportation (ie. for getting into Infected Zones, etc.).
|
||||
|
||||
### Item commands
|
||||
* `/itemN [type] [itemId] [amount]`
|
||||
(Refer to the [item list](https://docs.google.com/spreadsheets/d/1mpoJ9iTHl_xLI4wQ_9UvIDYNcsDYscdkyaGizs43TCg/))
|
||||
|
||||
### Nano commands
|
||||
* `/nano [id] (1-36)`
|
||||
* `/nano_equip [id] (1-36) [slot] (0-2)`
|
||||
* `/nano_unequip [slot] (0-2)`
|
||||
* `/nano_active [slot] (0-2)`
|
||||
|
||||
### A full list of commands can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).
|
||||
|
||||
99
config.ini
Normal file
99
config.ini
Normal file
@@ -0,0 +1,99 @@
|
||||
# verbosity level
|
||||
# 0 = mostly silence
|
||||
# 1 = debug prints and unknown packets
|
||||
# 2 = print all packets except LIVE_CHECK and movement
|
||||
# 3 = print all packets
|
||||
verbosity=1
|
||||
|
||||
# sandbox the process on supported platforms
|
||||
sandbox=true
|
||||
|
||||
# Login Server configuration
|
||||
[login]
|
||||
# must be kept in sync with loginInfo.php
|
||||
port=23000
|
||||
# will all 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=23001
|
||||
ip=127.0.0.1
|
||||
# distance at which other players and NPCs become visible.
|
||||
# this value is used for calculating chunk size
|
||||
viewdistance=16000
|
||||
# time, in milliseconds, to wait before kicking a non-responsive client
|
||||
# default is 1 minute
|
||||
timeout=60000
|
||||
# should mobs move around and fight back?
|
||||
# can be disabled for easier mob placement
|
||||
simulatemobs=true
|
||||
# little message players see when they enter the game
|
||||
motd=Welcome to OpenFusion!
|
||||
|
||||
# The following are the default locations of the JSON files the server
|
||||
# requires to run. You can override them by changing their values and
|
||||
# uncommenting them (removing the leading # character from that line).
|
||||
|
||||
# location of the tabledata folder
|
||||
#tdatadir=tdata/
|
||||
# location of the patch folder
|
||||
#patchdir=tdata/patch/
|
||||
|
||||
# Space-separated list of patch folders in patchdir to load from.
|
||||
# If you uncomment this, note that Academy builds *must* contain 1013,
|
||||
# and pre-Academy builds must *not* contain it.
|
||||
#enabledpatches=1013
|
||||
|
||||
# xdt json filename
|
||||
#xdtdata=xdt.json
|
||||
# NPC json filename
|
||||
#npcdata=NPCs.json
|
||||
# mob json filename
|
||||
#mobdata=mobs.json
|
||||
# path json filename
|
||||
#pathdata=paths.json
|
||||
# drop json filename
|
||||
#dropdata=drops.json
|
||||
# gruntwork output filename (this is what you submit)
|
||||
#gruntwork=gruntwork.json
|
||||
# location of the database
|
||||
#dbpath=database.db
|
||||
|
||||
# should 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
|
||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
openfusion:
|
||||
image: openfusion
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
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 |
BIN
res/radiorave_logo.png
Normal file
BIN
res/radiorave_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 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)
|
||||
)
|
||||
804
src/Abilities.cpp
Normal file
804
src/Abilities.cpp
Normal file
@@ -0,0 +1,804 @@
|
||||
#include "Abilities.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Player.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "Groups.hpp"
|
||||
#include "Eggs.hpp"
|
||||
|
||||
/*
|
||||
* TODO: This file is in desperate need of deduplication and rewriting.
|
||||
*/
|
||||
|
||||
std::map<int32_t, SkillData> Nanos::SkillTable;
|
||||
|
||||
/*
|
||||
* targetData approach
|
||||
* first integer is the count
|
||||
* second to fifth integers are IDs, these can be either player iID or mob's iID
|
||||
*/
|
||||
std::vector<int> Nanos::findTargets(Player* plr, int skillID, CNPacketData* data) {
|
||||
std::vector<int> tD(5);
|
||||
|
||||
if (SkillTable[skillID].targetType <= 2 && data != nullptr) { // client gives us the targets
|
||||
sP_CL2FE_REQ_NANO_SKILL_USE* pkt = (sP_CL2FE_REQ_NANO_SKILL_USE*)data->buf;
|
||||
|
||||
// validate request check
|
||||
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_NANO_SKILL_USE), pkt->iTargetCnt, sizeof(int32_t), data->size)) {
|
||||
std::cout << "[WARN] bad sP_CL2FE_REQ_NANO_SKILL_USE packet size" << std::endl;
|
||||
return tD;
|
||||
}
|
||||
|
||||
int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_NANO_SKILL_USE));
|
||||
tD[0] = pkt->iTargetCnt;
|
||||
|
||||
for (int i = 0; i < pkt->iTargetCnt; i++)
|
||||
tD[i+1] = pktdata[i];
|
||||
|
||||
} else if (SkillTable[skillID].targetType == 2) { // self target only
|
||||
tD[0] = 1;
|
||||
tD[1] = plr->iID;
|
||||
|
||||
} else if (SkillTable[skillID].targetType == 3) { // entire group as target
|
||||
Player *otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return tD;
|
||||
|
||||
if (SkillTable[skillID].effectArea == 0) { // for buffs
|
||||
tD[0] = otherPlr->groupCnt;
|
||||
for (int i = 0; i < otherPlr->groupCnt; i++)
|
||||
tD[i+1] = otherPlr->groupIDs[i];
|
||||
return tD;
|
||||
}
|
||||
|
||||
for (int i = 0; i < otherPlr->groupCnt; i++) { // group heals have an area limit
|
||||
Player *otherPlr2 = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
|
||||
if (otherPlr2 == nullptr)
|
||||
continue;
|
||||
if (true) {//hypot(otherPlr2->x - plr->x, otherPlr2->y - plr->y) < SkillTable[skillID].effectArea) {
|
||||
tD[i+1] = otherPlr->groupIDs[i];
|
||||
tD[0] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tD;
|
||||
}
|
||||
|
||||
void Nanos::nanoUnbuff(CNSocket* sock, std::vector<int> targetData, int32_t bitFlag, int16_t timeBuffID, int16_t amount, bool groupPower) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
plr->iSelfConditionBitFlag &= ~bitFlag;
|
||||
int groupFlags = 0;
|
||||
|
||||
if (groupPower) {
|
||||
plr->iGroupConditionBitFlag &= ~bitFlag;
|
||||
Player *leader = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
||||
if (leader != nullptr)
|
||||
groupFlags = Groups::getGroupFlags(leader);
|
||||
}
|
||||
|
||||
for (int i = 0; i < targetData[0]; i++) {
|
||||
Player* varPlr = PlayerManager::getPlayerFromID(targetData[i+1]);
|
||||
if (!((groupFlags | varPlr->iSelfConditionBitFlag) & bitFlag)) {
|
||||
CNSocket* sockTo = PlayerManager::getSockFromID(targetData[i+1]);
|
||||
if (sockTo == nullptr)
|
||||
continue; // sanity check
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp);
|
||||
resp.eCSTB = timeBuffID; // eCharStatusTimeBuffID
|
||||
resp.eTBU = 2; // eTimeBuffUpdate
|
||||
resp.eTBT = 1; // eTimeBuffType 1 means nano
|
||||
varPlr->iConditionBitFlag &= ~bitFlag;
|
||||
resp.iConditionBitFlag = varPlr->iConditionBitFlag |= groupFlags | varPlr->iSelfConditionBitFlag;
|
||||
|
||||
if (amount > 0)
|
||||
resp.TimeBuff.iValue = amount;
|
||||
|
||||
sockTo->sendPacket((void*)&resp, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Nanos::applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags) {
|
||||
if (SkillTable[skillID].drainType == 1)
|
||||
return 0;
|
||||
|
||||
int32_t bitFlag = 0;
|
||||
|
||||
for (auto& pwr : NanoPowers) {
|
||||
if (pwr.skillType == SkillTable[skillID].skillType) {
|
||||
bitFlag = pwr.bitFlag;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
if (eTBU == 1 || !((groupFlags | plr->iSelfConditionBitFlag) & bitFlag)) {
|
||||
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp);
|
||||
resp.eCSTB = pwr.timeBuffID;
|
||||
resp.eTBU = eTBU;
|
||||
resp.eTBT = eTBT;
|
||||
|
||||
if (eTBU == 1)
|
||||
plr->iConditionBitFlag |= bitFlag;
|
||||
else
|
||||
plr->iConditionBitFlag &= ~bitFlag;
|
||||
|
||||
resp.iConditionBitFlag = plr->iConditionBitFlag |= groupFlags | plr->iSelfConditionBitFlag;
|
||||
resp.TimeBuff.iValue = SkillTable[skillID].powerIntensity[0];
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
|
||||
}
|
||||
return bitFlag;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#pragma region Nano Powers
|
||||
namespace Nanos {
|
||||
|
||||
bool doDebuff(CNSocket *sock, sSkillResult_Buff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) {
|
||||
std::cout << "[WARN] doDebuff: NPC ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
BaseNPC* npc = NPCManager::NPCs[targetID];
|
||||
if (npc->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] doDebuff: NPC is not a mob" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
Mob* mob = (Mob*)npc;
|
||||
Combat::hitMob(sock, mob, 0);
|
||||
|
||||
respdata[i].eCT = 4;
|
||||
respdata[i].iID = mob->appearanceData.iNPC_ID;
|
||||
respdata[i].bProtected = 1;
|
||||
if (mob->skillStyle < 0 && mob->state != MobState::RETREAT
|
||||
&& !(mob->appearanceData.iConditionBitFlag & CSB_BIT_FREEDOM)) { // only debuff if the enemy is not retreating, casting corruption or in freedom
|
||||
mob->appearanceData.iConditionBitFlag |= bitFlag;
|
||||
mob->unbuffTimes[bitFlag] = getTime() + duration * 100;
|
||||
respdata[i].bProtected = 0;
|
||||
}
|
||||
respdata[i].iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doBuff(CNSocket *sock, sSkillResult_Buff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
Player *plr = nullptr;
|
||||
CNSocket *sockTo = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == targetID) {
|
||||
sockTo = pair.first;
|
||||
plr = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
if (sockTo == nullptr || plr == nullptr) {
|
||||
std::cout << "[WARN] doBuff: player ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
respdata[i].eCT = 1;
|
||||
respdata[i].iID = plr->iID;
|
||||
respdata[i].iConditionBitFlag = 0;
|
||||
|
||||
// only apply buffs if the player is actually alive
|
||||
if (plr->HP > 0) {
|
||||
respdata[i].iConditionBitFlag = bitFlag;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
|
||||
pkt.eCSTB = timeBuffID; // eCharStatusTimeBuffID
|
||||
pkt.eTBU = 1; // eTimeBuffUpdate
|
||||
pkt.eTBT = 1; // eTimeBuffType 1 means nano
|
||||
pkt.iConditionBitFlag = plr->iConditionBitFlag |= bitFlag;
|
||||
|
||||
if (amount > 0)
|
||||
pkt.TimeBuff.iValue = amount;
|
||||
|
||||
sockTo->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doDamageNDebuff(CNSocket *sock, sSkillResult_Damage_N_Debuff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) {
|
||||
// not sure how to best handle this
|
||||
std::cout << "[WARN] doDamageNDebuff: NPC ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
BaseNPC* npc = NPCManager::NPCs[targetID];
|
||||
if (npc->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] doDamageNDebuff: NPC is not a mob" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
Mob* mob = (Mob*)npc;
|
||||
|
||||
Combat::hitMob(sock, mob, 0); // just to gain aggro
|
||||
|
||||
respdata[i].eCT = 4;
|
||||
respdata[i].iDamage = duration / 10;
|
||||
respdata[i].iID = mob->appearanceData.iNPC_ID;
|
||||
respdata[i].iHP = mob->appearanceData.iHP;
|
||||
respdata[i].bProtected = 1;
|
||||
if (mob->skillStyle < 0 && mob->state != MobState::RETREAT
|
||||
&& !(mob->appearanceData.iConditionBitFlag & CSB_BIT_FREEDOM)) { // only debuff if the enemy is not retreating, casting corruption or in freedom
|
||||
mob->appearanceData.iConditionBitFlag |= bitFlag;
|
||||
mob->unbuffTimes[bitFlag] = getTime() + duration * 100;
|
||||
respdata[i].bProtected = 0;
|
||||
}
|
||||
respdata[i].iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doHeal(CNSocket *sock, sSkillResult_Heal_HP *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
Player *plr = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == targetID) {
|
||||
plr = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
if (plr == nullptr) {
|
||||
std::cout << "[WARN] doHeal: player ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
int healedAmount = PC_MAXHEALTH(plr->level) * amount / 1000;
|
||||
|
||||
// do not heal dead players
|
||||
if (plr->HP <= 0)
|
||||
healedAmount = 0;
|
||||
|
||||
plr->HP += healedAmount;
|
||||
|
||||
if (plr->HP > PC_MAXHEALTH(plr->level))
|
||||
plr->HP = PC_MAXHEALTH(plr->level);
|
||||
|
||||
respdata[i].eCT = 1;
|
||||
respdata[i].iID = plr->iID;
|
||||
respdata[i].iHP = plr->HP;
|
||||
respdata[i].iHealHP = healedAmount;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doDamage(CNSocket *sock, sSkillResult_Damage *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) {
|
||||
// not sure how to best handle this
|
||||
std::cout << "[WARN] doDamage: NPC ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
BaseNPC* npc = NPCManager::NPCs[targetID];
|
||||
if (npc->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] doDamage: NPC is not a mob" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
Mob* mob = (Mob*)npc;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int damage = Combat::hitMob(sock, mob, std::max(PC_MAXHEALTH(plr->level) * amount / 1000, mob->maxHealth * amount / 1000));
|
||||
|
||||
respdata[i].eCT = 4;
|
||||
respdata[i].iDamage = damage;
|
||||
respdata[i].iID = mob->appearanceData.iNPC_ID;
|
||||
respdata[i].iHP = mob->appearanceData.iHP;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE: Leech is specially encoded.
|
||||
*
|
||||
* It manages to fit inside the nanoPower<>() mold with only a slight hack,
|
||||
* but it really is it's own thing. There is a hard assumption that players
|
||||
* will only every leech a single mob, and the sanity check that enforces that
|
||||
* assumption is critical.
|
||||
*/
|
||||
|
||||
bool doLeech(CNSocket *sock, sSkillResult_Heal_HP *healdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
// this sanity check is VERY important
|
||||
if (i != 0) {
|
||||
std::cout << "[WARN] Player attempted to leech more than one mob!" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
sSkillResult_Damage *damagedata = (sSkillResult_Damage*)(((uint8_t*)healdata) + sizeof(sSkillResult_Heal_HP));
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int healedAmount = amount;
|
||||
|
||||
plr->HP += healedAmount;
|
||||
|
||||
if (plr->HP > PC_MAXHEALTH(plr->level))
|
||||
plr->HP = PC_MAXHEALTH(plr->level);
|
||||
|
||||
healdata->eCT = 1;
|
||||
healdata->iID = plr->iID;
|
||||
healdata->iHP = plr->HP;
|
||||
healdata->iHealHP = healedAmount;
|
||||
|
||||
if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) {
|
||||
// not sure how to best handle this
|
||||
std::cout << "[WARN] doLeech: NPC ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
BaseNPC* npc = NPCManager::NPCs[targetID];
|
||||
if (npc->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] doLeech: NPC is not a mob" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
Mob* mob = (Mob*)npc;
|
||||
|
||||
int damage = Combat::hitMob(sock, mob, amount * 2);
|
||||
|
||||
damagedata->eCT = 4;
|
||||
damagedata->iDamage = damage;
|
||||
damagedata->iID = mob->appearanceData.iNPC_ID;
|
||||
damagedata->iHP = mob->appearanceData.iHP;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doResurrect(CNSocket *sock, sSkillResult_Resurrect *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
Player *plr = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == targetID) {
|
||||
plr = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
if (plr == nullptr) {
|
||||
std::cout << "[WARN] doResurrect: player ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
respdata[i].eCT = 1;
|
||||
respdata[i].iID = plr->iID;
|
||||
respdata[i].iRegenHP = plr->HP;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doMove(CNSocket *sock, sSkillResult_Move *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
Player *plr = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == targetID) {
|
||||
plr = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
if (plr == nullptr) {
|
||||
std::cout << "[WARN] doMove: player ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
Player *plr2 = PlayerManager::getPlayer(sock);
|
||||
|
||||
respdata[i].eCT = 1;
|
||||
respdata[i].iID = plr->iID;
|
||||
respdata[i].iMapNum = plr2->recallInstance;
|
||||
respdata[i].iMoveX = plr2->recallX;
|
||||
respdata[i].iMoveY = plr2->recallY;
|
||||
respdata[i].iMoveZ = plr2->recallZ;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class sPAYLOAD,
|
||||
bool (*work)(CNSocket*,sPAYLOAD*,int,int32_t,int32_t,int16_t,int16_t,int16_t)>
|
||||
void nanoPower(CNSocket *sock, std::vector<int> targetData,
|
||||
int16_t nanoID, int16_t skillID, int16_t duration, int16_t amount,
|
||||
int16_t skillType, int32_t bitFlag, int16_t timeBuffID) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (skillType == EST_RETROROCKET_SELF || skillType == EST_RECALL) // rocket and self recall does not need any trailing structs
|
||||
targetData[0] = 0;
|
||||
|
||||
size_t resplen;
|
||||
// special case since leech is atypically encoded
|
||||
if (skillType == EST_BLOODSUCKING)
|
||||
resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) + sizeof(sSkillResult_Heal_HP) + sizeof(sSkillResult_Damage);
|
||||
else
|
||||
resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) + targetData[0] * sizeof(sPAYLOAD);
|
||||
|
||||
// validate response packet
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), targetData[0], sizeof(sPAYLOAD))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_NANO_SKILL_USE packet size" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_NANO_SKILL_USE_SUCC *resp = (sP_FE2CL_NANO_SKILL_USE_SUCC*)respbuf;
|
||||
sPAYLOAD *respdata = (sPAYLOAD*)(respbuf+sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC));
|
||||
|
||||
resp->iPC_ID = plr->iID;
|
||||
resp->iSkillID = skillID;
|
||||
resp->iNanoID = nanoID;
|
||||
resp->iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
|
||||
resp->eST = skillType;
|
||||
resp->iTargetCnt = targetData[0];
|
||||
|
||||
if (SkillTable[skillID].drainType == 2) {
|
||||
if (SkillTable[skillID].targetType >= 2)
|
||||
plr->iSelfConditionBitFlag |= bitFlag;
|
||||
if (SkillTable[skillID].targetType == 3)
|
||||
plr->iGroupConditionBitFlag |= bitFlag;
|
||||
}
|
||||
|
||||
for (int i = 0; i < targetData[0]; i++)
|
||||
if (!work(sock, respdata, i, targetData[i+1], bitFlag, timeBuffID, duration, amount))
|
||||
return;
|
||||
|
||||
sock->sendPacket((void*)&respbuf, P_FE2CL_NANO_SKILL_USE_SUCC, resplen);
|
||||
assert(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) == sizeof(sP_FE2CL_NANO_SKILL_USE));
|
||||
if (skillType == EST_RECALL_GROUP) { // in the case of group recall, nobody but group members need the packet
|
||||
for (int i = 0; i < targetData[0]; i++) {
|
||||
CNSocket *sock2 = PlayerManager::getSockFromID(targetData[i+1]);
|
||||
sock2->sendPacket((void*)&respbuf, P_FE2CL_NANO_SKILL_USE, resplen);
|
||||
}
|
||||
} else
|
||||
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NANO_SKILL_USE, resplen);
|
||||
|
||||
// Warping on recall
|
||||
if (skillType == EST_RECALL || skillType == EST_RECALL_GROUP) {
|
||||
if ((int32_t)plr->instanceID == plr->recallInstance)
|
||||
PlayerManager::sendPlayerTo(sock, plr->recallX, plr->recallY, plr->recallZ, plr->recallInstance);
|
||||
else {
|
||||
INITSTRUCT(sP_FE2CL_REP_WARP_USE_RECALL_FAIL, response)
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_WARP_USE_RECALL_FAIL, sizeof(sP_FE2CL_REP_WARP_USE_RECALL_FAIL));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nano power dispatch table
|
||||
std::vector<NanoPower> NanoPowers = {
|
||||
NanoPower(EST_STUN, CSB_BIT_STUN, ECSB_STUN, nanoPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
|
||||
NanoPower(EST_HEAL_HP, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Heal_HP, doHeal>),
|
||||
NanoPower(EST_BOUNDINGBALL, CSB_BIT_BOUNDINGBALL, ECSB_BOUNDINGBALL, nanoPower<sSkillResult_Buff, doDebuff>),
|
||||
NanoPower(EST_SNARE, CSB_BIT_DN_MOVE_SPEED, ECSB_DN_MOVE_SPEED, nanoPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
|
||||
NanoPower(EST_DAMAGE, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Damage, doDamage>),
|
||||
NanoPower(EST_BLOODSUCKING, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Heal_HP, doLeech>),
|
||||
NanoPower(EST_SLEEP, CSB_BIT_MEZ, ECSB_MEZ, nanoPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
|
||||
NanoPower(EST_REWARDBLOB, CSB_BIT_REWARD_BLOB, ECSB_REWARD_BLOB, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_RUN, CSB_BIT_UP_MOVE_SPEED, ECSB_UP_MOVE_SPEED, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_REWARDCASH, CSB_BIT_REWARD_CASH, ECSB_REWARD_CASH, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_PROTECTBATTERY, CSB_BIT_PROTECT_BATTERY, ECSB_PROTECT_BATTERY, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_MINIMAPENEMY, CSB_BIT_MINIMAP_ENEMY, ECSB_MINIMAP_ENEMY, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_PROTECTINFECTION, CSB_BIT_PROTECT_INFECTION, ECSB_PROTECT_INFECTION, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_JUMP, CSB_BIT_UP_JUMP_HEIGHT, ECSB_UP_JUMP_HEIGHT, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_FREEDOM, CSB_BIT_FREEDOM, ECSB_FREEDOM, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_PHOENIX, CSB_BIT_PHOENIX, ECSB_PHOENIX, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_STEALTH, CSB_BIT_UP_STEALTH, ECSB_UP_STEALTH, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_MINIMAPTRESURE, CSB_BIT_MINIMAP_TRESURE, ECSB_MINIMAP_TRESURE, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_RECALL, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Move, doMove>),
|
||||
NanoPower(EST_RECALL_GROUP, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Move, doMove>),
|
||||
NanoPower(EST_RETROROCKET_SELF, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_PHOENIX_GROUP, CSB_BIT_NONE, ECSB_NONE, nanoPower<sSkillResult_Resurrect, doResurrect>),
|
||||
NanoPower(EST_NANOSTIMPAK, CSB_BIT_STIMPAKSLOT1, ECSB_STIMPAKSLOT1, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_NANOSTIMPAK, CSB_BIT_STIMPAKSLOT2, ECSB_STIMPAKSLOT2, nanoPower<sSkillResult_Buff, doBuff>),
|
||||
NanoPower(EST_NANOSTIMPAK, CSB_BIT_STIMPAKSLOT3, ECSB_STIMPAKSLOT3, nanoPower<sSkillResult_Buff, doBuff>)
|
||||
};
|
||||
|
||||
}; // namespace
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Mob Powers
|
||||
namespace Combat {
|
||||
bool doDamageNDebuff(Mob *mob, sSkillResult_Damage_N_Debuff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
CNSocket *sock = nullptr;
|
||||
Player *plr = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == targetID) {
|
||||
sock = pair.first;
|
||||
plr = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
if (plr == nullptr) {
|
||||
std::cout << "[WARN] doDamageNDebuff: player ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
respdata[i].eCT = 1;
|
||||
respdata[i].iDamage = duration / 10;
|
||||
respdata[i].iID = plr->iID;
|
||||
respdata[i].iHP = plr->HP;
|
||||
respdata[i].iStamina = plr->Nanos[plr->activeNano].iStamina;
|
||||
if (plr->iConditionBitFlag & CSB_BIT_FREEDOM)
|
||||
respdata[i].bProtected = 1;
|
||||
else {
|
||||
if (!(plr->iConditionBitFlag & bitFlag)) {
|
||||
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
|
||||
pkt.eCSTB = timeBuffID; // eCharStatusTimeBuffID
|
||||
pkt.eTBU = 1; // eTimeBuffUpdate
|
||||
pkt.eTBT = 2;
|
||||
pkt.iConditionBitFlag = plr->iConditionBitFlag |= bitFlag;
|
||||
pkt.TimeBuff.iValue = amount * 5;
|
||||
sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
|
||||
}
|
||||
|
||||
respdata[i].bProtected = 0;
|
||||
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, bitFlag);
|
||||
time_t until = getTime() + (time_t)duration * 100;
|
||||
Eggs::EggBuffs[key] = until;
|
||||
}
|
||||
respdata[i].iConditionBitFlag = plr->iConditionBitFlag;
|
||||
|
||||
if (plr->HP <= 0) {
|
||||
mob->target = nullptr;
|
||||
mob->state = MobState::RETREAT;
|
||||
if (!MobAI::aggroCheck(mob, getTime())) {
|
||||
MobAI::clearDebuff(mob);
|
||||
if (mob->groupLeader != 0)
|
||||
MobAI::groupRetreat(mob);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doHeal(Mob *mob, sSkillResult_Heal_HP *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
if (NPCManager::NPCs.find(targetID) == NPCManager::NPCs.end()) {
|
||||
std::cout << "[WARN] doHeal: NPC ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
BaseNPC* npc = NPCManager::NPCs[targetID];
|
||||
if (npc->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] doHeal: NPC is not a mob" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
Mob* targetMob = (Mob*)npc;
|
||||
|
||||
int healedAmount = amount * targetMob->maxHealth / 1000;
|
||||
targetMob->appearanceData.iHP += healedAmount;
|
||||
if (targetMob->appearanceData.iHP > targetMob->maxHealth)
|
||||
targetMob->appearanceData.iHP = targetMob->maxHealth;
|
||||
|
||||
respdata[i].eCT = 4;
|
||||
respdata[i].iID = targetMob->appearanceData.iNPC_ID;
|
||||
respdata[i].iHP = targetMob->appearanceData.iHP;
|
||||
respdata[i].iHealHP = healedAmount;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doReturnHeal(Mob *mob, sSkillResult_Heal_HP *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
int healedAmount = amount * mob->maxHealth / 1000;
|
||||
mob->appearanceData.iHP += healedAmount;
|
||||
if (mob->appearanceData.iHP > mob->maxHealth)
|
||||
mob->appearanceData.iHP = mob->maxHealth;
|
||||
|
||||
respdata[i].eCT = 4;
|
||||
respdata[i].iID = mob->appearanceData.iNPC_ID;
|
||||
respdata[i].iHP = mob->appearanceData.iHP;
|
||||
respdata[i].iHealHP = healedAmount;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doDamage(Mob *mob, sSkillResult_Damage *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
Player *plr = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == targetID) {
|
||||
plr = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
if (plr == nullptr) {
|
||||
std::cout << "[WARN] doDamage: player ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
int damage = amount * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
|
||||
|
||||
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)
|
||||
damage = 0;
|
||||
|
||||
respdata[i].eCT = 1;
|
||||
respdata[i].iDamage = damage;
|
||||
respdata[i].iID = plr->iID;
|
||||
respdata[i].iHP = plr->HP -= damage;
|
||||
|
||||
if (plr->HP <= 0) {
|
||||
mob->target = nullptr;
|
||||
mob->state = MobState::RETREAT;
|
||||
if (!MobAI::aggroCheck(mob, getTime())) {
|
||||
MobAI::clearDebuff(mob);
|
||||
if (mob->groupLeader != 0)
|
||||
MobAI::groupRetreat(mob);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doLeech(Mob *mob, sSkillResult_Heal_HP *healdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
// this sanity check is VERY important
|
||||
if (i != 0) {
|
||||
std::cout << "[WARN] Mob attempted to leech more than one player!" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
Player *plr = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == targetID) {
|
||||
plr = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
if (plr == nullptr) {
|
||||
std::cout << "[WARN] doLeech: player ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
sSkillResult_Damage *damagedata = (sSkillResult_Damage*)(((uint8_t*)healdata) + sizeof(sSkillResult_Heal_HP));
|
||||
|
||||
int healedAmount = amount * PC_MAXHEALTH(plr->level) / 1000;
|
||||
|
||||
mob->appearanceData.iHP += healedAmount;
|
||||
if (mob->appearanceData.iHP > mob->maxHealth)
|
||||
mob->appearanceData.iHP = mob->maxHealth;
|
||||
|
||||
healdata->eCT = 4;
|
||||
healdata->iID = mob->appearanceData.iNPC_ID;
|
||||
healdata->iHP = mob->appearanceData.iHP;
|
||||
healdata->iHealHP = healedAmount;
|
||||
|
||||
int damage = healedAmount;
|
||||
|
||||
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)
|
||||
damage = 0;
|
||||
|
||||
damagedata->eCT = 1;
|
||||
damagedata->iDamage = damage;
|
||||
damagedata->iID = plr->iID;
|
||||
damagedata->iHP = plr->HP -= damage;
|
||||
|
||||
if (plr->HP <= 0) {
|
||||
mob->target = nullptr;
|
||||
mob->state = MobState::RETREAT;
|
||||
if (!MobAI::aggroCheck(mob, getTime())) {
|
||||
MobAI::clearDebuff(mob);
|
||||
if (mob->groupLeader != 0)
|
||||
MobAI::groupRetreat(mob);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doBatteryDrain(Mob *mob, sSkillResult_BatteryDrain *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
Player *plr = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == targetID) {
|
||||
plr = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
if (plr == nullptr) {
|
||||
std::cout << "[WARN] doBatteryDrain: player ID not found" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
respdata[i].eCT = 1;
|
||||
respdata[i].iID = plr->iID;
|
||||
|
||||
if (plr->iConditionBitFlag & CSB_BIT_PROTECT_BATTERY) {
|
||||
respdata[i].bProtected = 1;
|
||||
respdata[i].iDrainW = 0;
|
||||
respdata[i].iDrainN = 0;
|
||||
} else {
|
||||
respdata[i].bProtected = 0;
|
||||
respdata[i].iDrainW = amount * (18 + (int)mob->data["m_iNpcLevel"]) / 36;
|
||||
respdata[i].iDrainN = amount * (18 + (int)mob->data["m_iNpcLevel"]) / 36;
|
||||
}
|
||||
|
||||
respdata[i].iBatteryW = plr->batteryW -= (respdata[i].iDrainW < plr->batteryW) ? respdata[i].iDrainW : plr->batteryW;
|
||||
respdata[i].iBatteryN = plr->batteryN -= (respdata[i].iDrainN < plr->batteryN) ? respdata[i].iDrainN : plr->batteryN;
|
||||
respdata[i].iStamina = plr->Nanos[plr->activeNano].iStamina;
|
||||
respdata[i].iConditionBitFlag = plr->iConditionBitFlag;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool doBuff(Mob *mob, sSkillResult_Buff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) {
|
||||
respdata[i].eCT = 4;
|
||||
respdata[i].iID = mob->appearanceData.iNPC_ID;
|
||||
mob->appearanceData.iConditionBitFlag |= bitFlag;
|
||||
respdata[i].iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class sPAYLOAD,
|
||||
bool (*work)(Mob*,sPAYLOAD*,int,int32_t,int32_t,int16_t,int16_t,int16_t)>
|
||||
void mobPower(Mob *mob, std::vector<int> targetData,
|
||||
int16_t skillID, int16_t duration, int16_t amount,
|
||||
int16_t skillType, int32_t bitFlag, int16_t timeBuffID) {
|
||||
size_t resplen;
|
||||
// special case since leech is atypically encoded
|
||||
if (skillType == EST_BLOODSUCKING)
|
||||
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Heal_HP) + sizeof(sSkillResult_Damage);
|
||||
else
|
||||
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + targetData[0] * sizeof(sPAYLOAD);
|
||||
|
||||
// validate response packet
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), targetData[0], sizeof(sPAYLOAD))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_HIT packet size" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_NPC_SKILL_HIT *resp = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
|
||||
sPAYLOAD *respdata = (sPAYLOAD*)(respbuf+sizeof(sP_FE2CL_NPC_SKILL_HIT));
|
||||
|
||||
resp->iNPC_ID = mob->appearanceData.iNPC_ID;
|
||||
resp->iSkillID = skillID;
|
||||
resp->iValue1 = mob->hitX;
|
||||
resp->iValue2 = mob->hitY;
|
||||
resp->iValue3 = mob->hitZ;
|
||||
resp->eST = skillType;
|
||||
resp->iTargetCnt = targetData[0];
|
||||
|
||||
for (int i = 0; i < targetData[0]; i++)
|
||||
if (!work(mob, respdata, i, targetData[i+1], bitFlag, timeBuffID, duration, amount))
|
||||
return;
|
||||
|
||||
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
|
||||
}
|
||||
|
||||
// nano power dispatch table
|
||||
std::vector<MobPower> MobPowers = {
|
||||
MobPower(EST_STUN, CSB_BIT_STUN, ECSB_STUN, mobPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
|
||||
MobPower(EST_HEAL_HP, CSB_BIT_NONE, ECSB_NONE, mobPower<sSkillResult_Heal_HP, doHeal>),
|
||||
MobPower(EST_RETURNHOMEHEAL, CSB_BIT_NONE, ECSB_NONE, mobPower<sSkillResult_Heal_HP, doReturnHeal>),
|
||||
MobPower(EST_SNARE, CSB_BIT_DN_MOVE_SPEED, ECSB_DN_MOVE_SPEED, mobPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
|
||||
MobPower(EST_DAMAGE, CSB_BIT_NONE, ECSB_NONE, mobPower<sSkillResult_Damage, doDamage>),
|
||||
MobPower(EST_BATTERYDRAIN, CSB_BIT_NONE, ECSB_NONE, mobPower<sSkillResult_BatteryDrain, doBatteryDrain>),
|
||||
MobPower(EST_SLEEP, CSB_BIT_MEZ, ECSB_MEZ, mobPower<sSkillResult_Damage_N_Debuff, doDamageNDebuff>),
|
||||
MobPower(EST_BLOODSUCKING, CSB_BIT_NONE, ECSB_NONE, mobPower<sSkillResult_Heal_HP, doLeech>),
|
||||
MobPower(EST_FREEDOM, CSB_BIT_FREEDOM, ECSB_FREEDOM, mobPower<sSkillResult_Buff, doBuff>)
|
||||
};
|
||||
|
||||
}; // namespace
|
||||
#pragma endregion
|
||||
64
src/Abilities.hpp
Normal file
64
src/Abilities.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "Combat.hpp"
|
||||
|
||||
typedef void (*PowerHandler)(CNSocket*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
|
||||
|
||||
struct NanoPower {
|
||||
int16_t skillType;
|
||||
int32_t bitFlag;
|
||||
int16_t timeBuffID;
|
||||
PowerHandler handler;
|
||||
|
||||
NanoPower(int16_t s, int32_t b, int16_t t, PowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
|
||||
|
||||
void handle(CNSocket *sock, std::vector<int> targetData, int16_t nanoID, int16_t skillID, int16_t duration, int16_t amount) {
|
||||
if (handler == nullptr)
|
||||
return;
|
||||
|
||||
handler(sock, targetData, nanoID, skillID, duration, amount, skillType, bitFlag, timeBuffID);
|
||||
}
|
||||
};
|
||||
|
||||
typedef void (*MobPowerHandler)(Mob*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
|
||||
|
||||
struct MobPower {
|
||||
int16_t skillType;
|
||||
int32_t bitFlag;
|
||||
int16_t timeBuffID;
|
||||
MobPowerHandler handler;
|
||||
|
||||
MobPower(int16_t s, int32_t b, int16_t t, MobPowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
|
||||
|
||||
void handle(Mob *mob, std::vector<int> targetData, int16_t skillID, int16_t duration, int16_t amount) {
|
||||
if (handler == nullptr)
|
||||
return;
|
||||
|
||||
handler(mob, targetData, skillID, duration, amount, skillType, bitFlag, timeBuffID);
|
||||
}
|
||||
};
|
||||
|
||||
struct SkillData {
|
||||
int skillType;
|
||||
int targetType;
|
||||
int drainType;
|
||||
int effectArea;
|
||||
int batteryUse[4];
|
||||
int durationTime[4];
|
||||
int powerIntensity[4];
|
||||
};
|
||||
|
||||
namespace Nanos {
|
||||
extern std::vector<NanoPower> NanoPowers;
|
||||
extern std::map<int32_t, SkillData> SkillTable;
|
||||
|
||||
void nanoUnbuff(CNSocket* sock, std::vector<int> targetData, int32_t bitFlag, int16_t timeBuffID, int16_t amount, bool groupPower);
|
||||
int applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags);
|
||||
|
||||
std::vector<int> findTargets(Player* plr, int skillID, CNPacketData* data = nullptr);
|
||||
}
|
||||
|
||||
namespace Combat {
|
||||
extern std::vector<MobPower> MobPowers;
|
||||
}
|
||||
470
src/Buddies.cpp
Normal file
470
src/Buddies.cpp
Normal file
@@ -0,0 +1,470 @@
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "Buddies.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Buddies.hpp"
|
||||
#include "db/Database.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "db/Database.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
using namespace Buddies;
|
||||
|
||||
#pragma region Helper methods
|
||||
|
||||
static int getAvailableBuddySlot(Player* plr) {
|
||||
int slot = -1;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (plr->buddyIDs[i] == 0)
|
||||
return i;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
static bool playerHasBuddyWithID(Player* plr, int buddyID) {
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (plr->buddyIDs[i] == buddyID)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#pragma endregion
|
||||
|
||||
// Refresh buddy list
|
||||
void Buddies::refreshBuddyList(CNSocket* sock) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
int buddyCnt = Database::getNumBuddies(plr);
|
||||
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC), buddyCnt, sizeof(sBuddyBaseInfo))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC) + buddyCnt * sizeof(sBuddyBaseInfo);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC* resp = (sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC*)respbuf;
|
||||
sBuddyBaseInfo* respdata = (sBuddyBaseInfo*)(respbuf + sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC));
|
||||
|
||||
// base response fields
|
||||
resp->iBuddyCnt = buddyCnt;
|
||||
resp->iID = plr->iID;
|
||||
resp->iPCUID = plr->PCStyle.iPC_UID;
|
||||
resp->iListNum = 0; // ???
|
||||
|
||||
int buddyIndex = 0;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
int64_t buddyID = plr->buddyIDs[i];
|
||||
if (buddyID != 0) {
|
||||
sBuddyBaseInfo buddyInfo = {};
|
||||
Player buddyPlayerData = {};
|
||||
Database::getPlayer(&buddyPlayerData, buddyID);
|
||||
if (buddyPlayerData.iID == 0)
|
||||
continue;
|
||||
buddyInfo.bBlocked = plr->isBuddyBlocked[i];
|
||||
buddyInfo.bFreeChat = 1;
|
||||
buddyInfo.iGender = buddyPlayerData.PCStyle.iGender;
|
||||
buddyInfo.iID = buddyID;
|
||||
buddyInfo.iPCUID = buddyID;
|
||||
buddyInfo.iNameCheckFlag = buddyPlayerData.PCStyle.iNameCheck;
|
||||
buddyInfo.iPCState = buddyPlayerData.iPCState;
|
||||
memcpy(buddyInfo.szFirstName, buddyPlayerData.PCStyle.szFirstName, sizeof(buddyInfo.szFirstName));
|
||||
memcpy(buddyInfo.szLastName, buddyPlayerData.PCStyle.szLastName, sizeof(buddyInfo.szLastName));
|
||||
respdata[buddyIndex] = buddyInfo;
|
||||
buddyIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC, resplen);
|
||||
}
|
||||
|
||||
// Buddy request
|
||||
static void requestBuddy(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_REQUEST_MAKE_BUDDY*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(req->iBuddyID);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
if (getAvailableBuddySlot(plr) == -1 || getAvailableBuddySlot(otherPlr) == -1)
|
||||
{
|
||||
INITSTRUCT(sP_FE2CL_REP_REQUEST_MAKE_BUDDY_FAIL, failResp);
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_REQUEST_MAKE_BUDDY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(otherPlr->iID);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC, resp);
|
||||
INITSTRUCT(sP_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC_TO_ACCEPTER, otherResp);
|
||||
|
||||
resp.iRequestID = plr->iID;
|
||||
resp.iBuddyID = req->iBuddyID;
|
||||
resp.iBuddyPCUID = req->iBuddyPCUID;
|
||||
|
||||
otherResp.iRequestID = plr->iID;
|
||||
otherResp.iBuddyID = req->iBuddyID;
|
||||
memcpy(otherResp.szFirstName, plr->PCStyle.szFirstName, sizeof(plr->PCStyle.szFirstName));
|
||||
memcpy(otherResp.szLastName, plr->PCStyle.szLastName, sizeof(plr->PCStyle.szLastName));
|
||||
|
||||
std::cout << "Buddy ID: " << req->iBuddyID << std::endl;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC);
|
||||
otherSock->sendPacket(otherResp, P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC_TO_ACCEPTER);
|
||||
|
||||
}
|
||||
|
||||
// Sending buddy request by player name
|
||||
static void reqBuddyByName(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY*)data->buf;
|
||||
Player* plrReq = PlayerManager::getPlayer(sock);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_SUCC, resp);
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromName(AUTOU16TOU8(pkt->szFirstName), AUTOU16TOU8(pkt->szLastName));
|
||||
if (otherSock == nullptr)
|
||||
return; // no player found
|
||||
|
||||
Player *otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
if (playerHasBuddyWithID(plrReq, otherPlr->iID))
|
||||
return;
|
||||
|
||||
resp.iPCUID = plrReq->PCStyle.iPC_UID;
|
||||
resp.iNameCheckFlag = plrReq->PCStyle.iNameCheck;
|
||||
|
||||
memcpy(resp.szFirstName, plrReq->PCStyle.szFirstName, sizeof(plrReq->PCStyle.szFirstName));
|
||||
memcpy(resp.szLastName, plrReq->PCStyle.szLastName, sizeof(plrReq->PCStyle.szLastName));
|
||||
otherSock->sendPacket(resp, P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_SUCC);
|
||||
}
|
||||
|
||||
// Accepting buddy request
|
||||
static void reqAcceptBuddy(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_ACCEPT_MAKE_BUDDY*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(req->iBuddyID);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return; // sanity check
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(otherPlr->iID);
|
||||
|
||||
int slotA = getAvailableBuddySlot(plr);
|
||||
int slotB = getAvailableBuddySlot(otherPlr);
|
||||
if (slotA == -1 || slotB == -1)
|
||||
return; // sanity check
|
||||
|
||||
if (req->iAcceptFlag == 1 && plr->iID != otherPlr->iID && !playerHasBuddyWithID(plr, otherPlr->iID))
|
||||
{
|
||||
INITSTRUCT(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC, resp);
|
||||
|
||||
// A to B
|
||||
resp.iBuddySlot = slotA;
|
||||
resp.BuddyInfo.iID = otherPlr->iID;
|
||||
resp.BuddyInfo.iPCUID = otherPlr->PCStyle.iPC_UID;
|
||||
resp.BuddyInfo.iPCState = 1; // assumed to be online
|
||||
resp.BuddyInfo.bBlocked = 0; // not blocked by default
|
||||
resp.BuddyInfo.iGender = otherPlr->PCStyle.iGender; // shows the other player's gender
|
||||
resp.BuddyInfo.bFreeChat = 1; // shows whether or not the other player has freechat on (hardcoded for now)
|
||||
resp.BuddyInfo.iNameCheckFlag = otherPlr->PCStyle.iNameCheck;
|
||||
memcpy(resp.BuddyInfo.szFirstName, otherPlr->PCStyle.szFirstName, sizeof(resp.BuddyInfo.szFirstName));
|
||||
memcpy(resp.BuddyInfo.szLastName, otherPlr->PCStyle.szLastName, sizeof(resp.BuddyInfo.szLastName));
|
||||
sock->sendPacket(resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC);
|
||||
plr->buddyIDs[slotA] = otherPlr->PCStyle.iPC_UID;
|
||||
//std::cout << "Buddy's ID: " << plr->buddyIDs[slotA] << std::endl;
|
||||
|
||||
// B to A, using the same struct
|
||||
resp.iBuddySlot = slotB;
|
||||
resp.BuddyInfo.iID = plr->iID;
|
||||
resp.BuddyInfo.iPCUID = plr->PCStyle.iPC_UID;
|
||||
resp.BuddyInfo.iPCState = 1;
|
||||
resp.BuddyInfo.bBlocked = 0;
|
||||
resp.BuddyInfo.iGender = plr->PCStyle.iGender;
|
||||
resp.BuddyInfo.bFreeChat = 1;
|
||||
resp.BuddyInfo.iNameCheckFlag = plr->PCStyle.iNameCheck;
|
||||
memcpy(resp.BuddyInfo.szFirstName, plr->PCStyle.szFirstName, sizeof(resp.BuddyInfo.szFirstName));
|
||||
memcpy(resp.BuddyInfo.szLastName, plr->PCStyle.szLastName, sizeof(resp.BuddyInfo.szLastName));
|
||||
otherSock->sendPacket(resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC);
|
||||
otherPlr->buddyIDs[slotB] = plr->PCStyle.iPC_UID;
|
||||
//std::cout << "Buddy's ID: " << plr->buddyIDs[slotB] << std::endl;
|
||||
|
||||
// add record to db
|
||||
Database::addBuddyship(plr->iID, otherPlr->iID);
|
||||
}
|
||||
else
|
||||
{
|
||||
INITSTRUCT(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL, declineResp);
|
||||
|
||||
declineResp.iErrorCode = 6; // Buddy declined notification
|
||||
declineResp.iBuddyID = req->iBuddyID;
|
||||
declineResp.iBuddyPCUID = req->iBuddyPCUID;
|
||||
|
||||
otherSock->sendPacket(declineResp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Accepting buddy request from the find name request
|
||||
static void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY*)data->buf;
|
||||
|
||||
Player* plrReq = PlayerManager::getPlayer(sock);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC, resp);
|
||||
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(pkt->iBuddyPCUID);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
|
||||
|
||||
int slotA = getAvailableBuddySlot(plrReq);
|
||||
int slotB = getAvailableBuddySlot(otherPlr);
|
||||
if (slotA == -1 || slotB == -1)
|
||||
return; // sanity check
|
||||
|
||||
if (pkt->iAcceptFlag == 1 && plrReq->iID != otherPlr->iID && !playerHasBuddyWithID(plrReq, otherPlr->iID)) {
|
||||
INITSTRUCT(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC, resp);
|
||||
|
||||
// A to B
|
||||
resp.iBuddySlot = slotA;
|
||||
resp.BuddyInfo.iID = otherPlr->iID;
|
||||
resp.BuddyInfo.iPCUID = otherPlr->PCStyle.iPC_UID;
|
||||
resp.BuddyInfo.iPCState = 1; // assumed to be online
|
||||
resp.BuddyInfo.bBlocked = 0; // not blocked by default
|
||||
resp.BuddyInfo.iGender = otherPlr->PCStyle.iGender; // shows the other player's gender
|
||||
resp.BuddyInfo.bFreeChat = 1; // shows whether or not the other player has freechat on (hardcoded for now)
|
||||
resp.BuddyInfo.iNameCheckFlag = otherPlr->PCStyle.iNameCheck;
|
||||
memcpy(resp.BuddyInfo.szFirstName, otherPlr->PCStyle.szFirstName, sizeof(resp.BuddyInfo.szFirstName));
|
||||
memcpy(resp.BuddyInfo.szLastName, otherPlr->PCStyle.szLastName, sizeof(resp.BuddyInfo.szLastName));
|
||||
sock->sendPacket(resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC);
|
||||
plrReq->buddyIDs[slotA] = otherPlr->PCStyle.iPC_UID;
|
||||
//std::cout << "Buddy's ID: " << plr->buddyIDs[slotA] << std::endl;
|
||||
|
||||
// B to A, using the same struct
|
||||
resp.iBuddySlot = slotB;
|
||||
resp.BuddyInfo.iID = plrReq->iID;
|
||||
resp.BuddyInfo.iPCUID = plrReq->PCStyle.iPC_UID;
|
||||
resp.BuddyInfo.iPCState = 1;
|
||||
resp.BuddyInfo.bBlocked = 0;
|
||||
resp.BuddyInfo.iGender = plrReq->PCStyle.iGender;
|
||||
resp.BuddyInfo.bFreeChat = 1;
|
||||
resp.BuddyInfo.iNameCheckFlag = plrReq->PCStyle.iNameCheck;
|
||||
memcpy(resp.BuddyInfo.szFirstName, plrReq->PCStyle.szFirstName, sizeof(resp.BuddyInfo.szFirstName));
|
||||
memcpy(resp.BuddyInfo.szLastName, plrReq->PCStyle.szLastName, sizeof(resp.BuddyInfo.szLastName));
|
||||
otherSock->sendPacket(resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC);
|
||||
otherPlr->buddyIDs[slotB] = plrReq->PCStyle.iPC_UID;
|
||||
//std::cout << "Buddy's ID: " << plr->buddyIDs[slotB] << std::endl;
|
||||
|
||||
// add record to db
|
||||
Database::addBuddyship(plrReq->iID, otherPlr->iID);
|
||||
}
|
||||
else
|
||||
{
|
||||
INITSTRUCT(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL, declineResp);
|
||||
|
||||
declineResp.iErrorCode = 6; // Buddy declined notification
|
||||
declineResp.iBuddyPCUID = pkt->iBuddyPCUID;
|
||||
otherSock->sendPacket(declineResp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Getting buddy state
|
||||
static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
/*
|
||||
* If the buddy list wasn't synced a second time yet, sync it.
|
||||
* Not sure why we have to do it again for the client not to trip up.
|
||||
*/
|
||||
if (!plr->buddiesSynced) {
|
||||
refreshBuddyList(sock);
|
||||
plr->buddiesSynced = true;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);
|
||||
|
||||
for (int slot = 0; slot < 50; slot++) {
|
||||
resp.aBuddyState[slot] = PlayerManager::getPlayerFromID(plr->buddyIDs[slot]) != nullptr ? 1 : 0;
|
||||
resp.aBuddyID[slot] = plr->buddyIDs[slot];
|
||||
}
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_GET_BUDDY_STATE_SUCC);
|
||||
}
|
||||
|
||||
// Blocking the buddy
|
||||
static void reqBuddyBlock(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_SET_BUDDY_BLOCK*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// sanity checks
|
||||
if (pkt->iBuddySlot < 0 || pkt->iBuddySlot >= 50 || plr->buddyIDs[pkt->iBuddySlot] != pkt->iBuddyPCUID)
|
||||
return;
|
||||
|
||||
// save in DB
|
||||
Database::removeBuddyship(plr->iID, pkt->iBuddyPCUID);
|
||||
Database::addBlock(plr->iID, pkt->iBuddyPCUID);
|
||||
|
||||
// save serverside
|
||||
// since ID is already in the array, just set it to blocked
|
||||
plr->isBuddyBlocked[pkt->iBuddySlot] = true;
|
||||
|
||||
// send response
|
||||
INITSTRUCT(sP_FE2CL_REP_SET_BUDDY_BLOCK_SUCC, resp);
|
||||
resp.iBuddyPCUID = pkt->iBuddyPCUID;
|
||||
resp.iBuddySlot = pkt->iBuddySlot;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_SET_BUDDY_BLOCK_SUCC);
|
||||
|
||||
// notify the other player he isn't a buddy anymore
|
||||
INITSTRUCT(sP_FE2CL_REP_REMOVE_BUDDY_SUCC, otherResp);
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
|
||||
if (otherSock == nullptr)
|
||||
return; // other player isn't online, no broadcast needed
|
||||
Player* otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
// search for the slot with the requesting player's ID
|
||||
otherResp.iBuddyPCUID = plr->PCStyle.iPC_UID;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (otherPlr->buddyIDs[i] == plr->PCStyle.iPC_UID) {
|
||||
// remove buddy
|
||||
otherPlr->buddyIDs[i] = 0;
|
||||
// broadcast
|
||||
otherResp.iBuddySlot = i;
|
||||
otherSock->sendPacket(otherResp, P_FE2CL_REP_REMOVE_BUDDY_SUCC);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// block non-buddy
|
||||
static void reqPlayerBlock(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_SET_PC_BLOCK*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
int buddySlot = getAvailableBuddySlot(plr);
|
||||
if (buddySlot == -1)
|
||||
return;
|
||||
|
||||
// save in DB
|
||||
Database::addBlock(plr->iID, pkt->iBlock_PCUID);
|
||||
|
||||
// save serverside
|
||||
plr->buddyIDs[buddySlot] = pkt->iBlock_PCUID;
|
||||
plr->isBuddyBlocked[buddySlot] = true;
|
||||
|
||||
// send response
|
||||
INITSTRUCT(sP_FE2CL_REP_SET_PC_BLOCK_SUCC, resp);
|
||||
resp.iBlock_ID = pkt->iBlock_ID;
|
||||
resp.iBlock_PCUID = pkt->iBlock_PCUID;
|
||||
resp.iBuddySlot = buddySlot;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_SET_PC_BLOCK_SUCC);
|
||||
}
|
||||
|
||||
// Deleting the buddy
|
||||
static void reqBuddyDelete(CNSocket* sock, CNPacketData* data) {
|
||||
// note! this packet is used both for removing buddies and blocks
|
||||
auto pkt = (sP_CL2FE_REQ_REMOVE_BUDDY*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// remove buddy on our side
|
||||
INITSTRUCT(sP_FE2CL_REP_REMOVE_BUDDY_SUCC, resp);
|
||||
resp.iBuddyPCUID = pkt->iBuddyPCUID;
|
||||
resp.iBuddySlot = pkt->iBuddySlot;
|
||||
if (pkt->iBuddySlot < 0 || pkt->iBuddySlot >= 50 || plr->buddyIDs[pkt->iBuddySlot] != pkt->iBuddyPCUID)
|
||||
return; // sanity check
|
||||
|
||||
bool wasBlocked = plr->isBuddyBlocked[resp.iBuddySlot];
|
||||
plr->buddyIDs[resp.iBuddySlot] = 0;
|
||||
plr->isBuddyBlocked[resp.iBuddySlot] = false;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_REMOVE_BUDDY_SUCC);
|
||||
|
||||
// remove record from db
|
||||
Database::removeBuddyship(plr->PCStyle.iPC_UID, pkt->iBuddyPCUID);
|
||||
// try this too
|
||||
Database::removeBlock(plr->PCStyle.iPC_UID, pkt->iBuddyPCUID);
|
||||
|
||||
if (wasBlocked)
|
||||
return;
|
||||
|
||||
// remove buddy on their side, reusing the struct
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
|
||||
if (otherSock == nullptr)
|
||||
return; // other player isn't online, no broadcast needed
|
||||
Player* otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
// search for the slot with the requesting player's ID
|
||||
resp.iBuddyPCUID = plr->PCStyle.iPC_UID;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (otherPlr->buddyIDs[i] == plr->PCStyle.iPC_UID) {
|
||||
// remove buddy
|
||||
otherPlr->buddyIDs[i] = 0;
|
||||
// broadcast
|
||||
resp.iBuddySlot = i;
|
||||
otherSock->sendPacket(resp, P_FE2CL_REP_REMOVE_BUDDY_SUCC);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warping to buddy
|
||||
static void reqBuddyWarp(CNSocket* sock, CNPacketData* data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto pkt = (sP_CL2FE_REQ_PC_BUDDY_WARP*)data->buf;
|
||||
|
||||
if (pkt->iSlotNum < 0 || pkt->iSlotNum >= 50)
|
||||
return; // sanity check
|
||||
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(pkt->iBuddyPCUID);
|
||||
if (otherPlr == nullptr)
|
||||
return; // buddy offline
|
||||
|
||||
// if the player is instanced; no warp allowed
|
||||
if (otherPlr->instanceID != INSTANCE_OVERWORLD)
|
||||
goto fail;
|
||||
|
||||
// check if the players are at the same point in time (or in the training area or not)
|
||||
if (otherPlr->PCStyle2.iPayzoneFlag != plr->PCStyle2.iPayzoneFlag)
|
||||
goto fail;
|
||||
|
||||
// do not warp to players on monkeys
|
||||
if (otherPlr->onMonkey)
|
||||
goto fail;
|
||||
|
||||
// does the player disallow warping?
|
||||
if (otherPlr->unwarpable)
|
||||
goto fail;
|
||||
|
||||
// otherPlr->instanceID should always be INSTANCE_OVERWORLD at this point
|
||||
PlayerManager::sendPlayerTo(sock, otherPlr->x, otherPlr->y, otherPlr->z, otherPlr->instanceID);
|
||||
return;
|
||||
|
||||
fail:
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_BUDDY_WARP_FAIL, resp);
|
||||
resp.iBuddyPCUID = pkt->iBuddyPCUID;
|
||||
resp.iErrorCode = 0;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_BUDDY_WARP_FAIL);
|
||||
}
|
||||
|
||||
void Buddies::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REQUEST_MAKE_BUDDY, requestBuddy);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY, reqBuddyByName);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ACCEPT_MAKE_BUDDY, reqAcceptBuddy);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY, reqFindNameBuddyAccept);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_GET_BUDDY_STATE, reqPktGetBuddyState);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SET_BUDDY_BLOCK, reqBuddyBlock);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SET_PC_BLOCK, reqPlayerBlock);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REMOVE_BUDDY, reqBuddyDelete);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BUDDY_WARP, reqBuddyWarp);
|
||||
}
|
||||
12
src/Buddies.hpp
Normal file
12
src/Buddies.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "core/Core.hpp"
|
||||
#include "core/Core.hpp"
|
||||
|
||||
namespace Buddies {
|
||||
void init();
|
||||
|
||||
// Buddy list
|
||||
void refreshBuddyList(CNSocket* sock);
|
||||
}
|
||||
372
src/BuiltinCommands.cpp
Normal file
372
src/BuiltinCommands.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
#include "BuiltinCommands.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Chat.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "Rand.hpp"
|
||||
|
||||
// helper function, not a packet handler
|
||||
void BuiltinCommands::setSpecialState(CNSocket* sock, CNPacketData* data) {
|
||||
auto setData = (sP_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH*)data->buf;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// HACK: work around the invisible weapon bug
|
||||
if (setData->iSpecialStateFlag == CN_SPECIAL_STATE_FLAG__FULL_UI)
|
||||
Items::updateEquips(sock, plr);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_SPECIAL_STATE_CHANGE, response);
|
||||
|
||||
plr->iSpecialState ^= setData->iSpecialStateFlag;
|
||||
|
||||
response.iPC_ID = setData->iPC_ID;
|
||||
response.iReqSpecialStateFlag = setData->iSpecialStateFlag;
|
||||
response.iSpecialState = plr->iSpecialState;
|
||||
|
||||
sock->sendPacket(response, P_FE2CL_REP_PC_SPECIAL_STATE_SWITCH_SUCC);
|
||||
PlayerManager::sendToViewable(sock, response, P_FE2CL_PC_SPECIAL_STATE_CHANGE);
|
||||
}
|
||||
|
||||
static void setGMSpecialSwitchPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
if (PlayerManager::getPlayer(sock)->accountLevel > 30)
|
||||
return;
|
||||
|
||||
BuiltinCommands::setSpecialState(sock, data);
|
||||
}
|
||||
|
||||
static void gotoPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
if (plr->accountLevel > 50)
|
||||
return;
|
||||
|
||||
auto gotoData = (sP_CL2FE_REQ_PC_GOTO*)data->buf;
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "P_CL2FE_REQ_PC_GOTO:" << std::endl;
|
||||
std::cout << "\tX: " << gotoData->iToX << std::endl;
|
||||
std::cout << "\tY: " << gotoData->iToY << std::endl;
|
||||
std::cout << "\tZ: " << gotoData->iToZ << std::endl;
|
||||
)
|
||||
|
||||
PlayerManager::sendPlayerTo(sock, gotoData->iToX, gotoData->iToY, gotoData->iToZ, INSTANCE_OVERWORLD);
|
||||
}
|
||||
|
||||
static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
if (plr->accountLevel > 50)
|
||||
return;
|
||||
|
||||
auto setData = (sP_CL2FE_GM_REQ_PC_SET_VALUE*)data->buf;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_SET_VALUE, response);
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "P_CL2FE_GM_REQ_PC_SET_VALUE:" << std::endl;
|
||||
std::cout << "\tPC_ID: " << setData->iPC_ID << std::endl;
|
||||
std::cout << "\tSetValueType: " << setData->iSetValueType << std::endl;
|
||||
std::cout << "\tSetValue: " << setData->iSetValue << std::endl;
|
||||
)
|
||||
|
||||
// Handle serverside value-changes
|
||||
switch (setData->iSetValueType) {
|
||||
case 1:
|
||||
response.iSetValue = plr->HP = setData->iSetValue;
|
||||
break;
|
||||
case 2:
|
||||
plr->batteryW = setData->iSetValue;
|
||||
|
||||
// caps
|
||||
if (plr->batteryW > 9999)
|
||||
plr->batteryW = 9999;
|
||||
|
||||
response.iSetValue = plr->batteryW;
|
||||
break;
|
||||
case 3:
|
||||
plr->batteryN = setData->iSetValue;
|
||||
|
||||
// caps
|
||||
if (plr->batteryN > 9999)
|
||||
plr->batteryN = 9999;
|
||||
|
||||
response.iSetValue = plr->batteryN;
|
||||
break;
|
||||
case 4:
|
||||
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
|
||||
response.iSetValue = plr->fusionmatter;
|
||||
break;
|
||||
case 5:
|
||||
response.iSetValue = plr->money = 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 (req->eTeleportType) {
|
||||
case eCN_GM_TeleportMapType__MyLocation:
|
||||
PlayerManager::sendPlayerTo(targetSock, plr->x, plr->y, plr->z, instance);
|
||||
break;
|
||||
case eCN_GM_TeleportMapType__MapXYZ:
|
||||
instance = req->iToMap;
|
||||
// fallthrough
|
||||
case eCN_GM_TeleportMapType__XYZ:
|
||||
PlayerManager::sendPlayerTo(targetSock, req->iToX, req->iToY, req->iToZ, instance);
|
||||
break;
|
||||
case eCN_GM_TeleportMapType__SomeoneLocation:
|
||||
// player to teleport to
|
||||
goalSock = PlayerManager::getSockFromAny(req->eGoalPCSearchBy, req->iGoalPC_ID, req->iGoalPC_UID,
|
||||
AUTOU16TOU8(req->szGoalPC_FirstName), AUTOU16TOU8(req->szGoalPC_LastName));
|
||||
if (goalSock == nullptr) {
|
||||
Chat::sendServerMessage(sock, "teleportation target player not found");
|
||||
return;
|
||||
}
|
||||
goalPlr = PlayerManager::getPlayer(goalSock);
|
||||
|
||||
PlayerManager::sendPlayerTo(targetSock, goalPlr->x, goalPlr->y, goalPlr->z, goalPlr->instanceID);
|
||||
break;
|
||||
case eCN_GM_TeleportMapType__Unstick:
|
||||
targetPlr = PlayerManager::getPlayer(targetSock);
|
||||
|
||||
PlayerManager::sendPlayerTo(targetSock, targetPlr->x - unstickRange/2 + Rand::rand(unstickRange),
|
||||
targetPlr->y - unstickRange/2 + Rand::rand(unstickRange), targetPlr->z + 80);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void itemGMGiveHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto itemreq = (sP_CL2FE_REQ_PC_GIVE_ITEM*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (plr->accountLevel > 50) {
|
||||
// TODO: send fail packet
|
||||
return;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp);
|
||||
|
||||
if (itemreq->eIL == 1) {
|
||||
|
||||
if (Items::ItemData.find(std::pair<int32_t, int32_t>(itemreq->Item.iID, itemreq->Item.iType)) == Items::ItemData.end()
|
||||
|| itemreq->Item.iType < 0 || itemreq->Item.iType > 10) {
|
||||
// invalid item
|
||||
std::cout << "[WARN] Item id " << itemreq->Item.iID << " with type " << itemreq->Item.iType << " is invalid (give item)" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemreq->Item.iType == 10) {
|
||||
// item is vehicle, set expiration date
|
||||
// set time limit: current time + 7days
|
||||
itemreq->Item.iTimeLimit = getTimestamp() + 604800;
|
||||
}
|
||||
|
||||
plr->Inven[itemreq->iSlotNum] = itemreq->Item;
|
||||
} else if (itemreq->eIL == 2) {
|
||||
int id = itemreq->Item.iID;
|
||||
int slot = Missions::findQSlot(plr, id);
|
||||
|
||||
if (slot == -1) {
|
||||
std::cout << "[WARN] Player has no room for quest items" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (id != 0)
|
||||
std::cout << "new qitem in slot " << slot << std::endl;
|
||||
|
||||
// update player
|
||||
if (id != 0) {
|
||||
plr->QInven[slot].iType = 8;
|
||||
plr->QInven[slot].iID = id;
|
||||
plr->QInven[slot].iOpt += itemreq->Item.iOpt;
|
||||
|
||||
// destroy the item if its 0
|
||||
if (plr->QInven[slot].iOpt == 0)
|
||||
memset(&plr->QInven[slot], 0, sizeof(sItemBase));
|
||||
}
|
||||
std::cout << "Item id " << id << " is in slot " << slot << " of count " << plr->QInven[slot].iOpt << std::endl;
|
||||
}
|
||||
|
||||
resp.eIL = itemreq->eIL;
|
||||
resp.iSlotNum = itemreq->iSlotNum;
|
||||
resp.Item = itemreq->Item;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
|
||||
}
|
||||
|
||||
static void nanoGMGiveHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto nano = (sP_CL2FE_REQ_PC_GIVE_NANO*)data->buf;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (plr->accountLevel > 50)
|
||||
return;
|
||||
|
||||
// Add nano to player
|
||||
Nanos::addNano(sock, nano->iNanoID, 0);
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << PlayerManager::getPlayerName(plr) << " requested to add nano id: " << nano->iNanoID << std::endl;
|
||||
)
|
||||
}
|
||||
|
||||
void BuiltinCommands::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GOTO, gotoPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SET_VALUE, setValuePlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_ITEM, itemGMGiveHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_NANO, nanoGMGiveHandler);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH, setGMSpecialSwitchPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF, setGMSpecialOnOff);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_LOCATION, locatePlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_KICK_PLAYER, kickPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_TO_PC, warpToPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_TARGET_PC_TELEPORT, teleportPlayer);
|
||||
}
|
||||
9
src/BuiltinCommands.hpp
Normal file
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,244 +0,0 @@
|
||||
#include "CNLoginServer.hpp"
|
||||
#include "CNShared.hpp"
|
||||
#include "CNStructs.hpp"
|
||||
|
||||
#include "settings.hpp"
|
||||
|
||||
/*
|
||||
This is *not* connected to any database, so i'm sending dummy data just to get the client to work & connect to the shard <3
|
||||
*/
|
||||
|
||||
std::map<CNSocket*, CNLoginData> CNLoginServer::loginSessions;
|
||||
|
||||
CNLoginServer::CNLoginServer(uint16_t p) {
|
||||
port = p;
|
||||
pHandler = &CNLoginServer::handlePacket;
|
||||
init();
|
||||
}
|
||||
|
||||
void CNLoginServer::handlePacket(CNSocket* sock, CNPacketData* data) {
|
||||
switch (data->type) {
|
||||
case P_CL2LS_REQ_LOGIN: {
|
||||
sP_CL2LS_REQ_LOGIN* login = (sP_CL2LS_REQ_LOGIN*)data->buf;
|
||||
sP_LS2CL_REP_LOGIN_SUCC* response = (sP_LS2CL_REP_LOGIN_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_LOGIN_SUCC));
|
||||
uint64_t cachedKey = sock->getEKey(); // so we can still send the response packet with the correct key
|
||||
int charCount = 2; // send 4 randomly generated characters for now
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "P_CL2LS_REQ_LOGIN:" << std::endl;
|
||||
std::cout << "\tClient version: " << login->iClientVerA << "." << login->iClientVerB << "." << login->iClientVerC << std::endl;
|
||||
std::cout << "\tLogin type: " << login->iLoginType << std::endl;
|
||||
std::cout << "\tID: " << U16toU8(login->szID) << " Password: " << U16toU8(login->szPassword) << std::endl;
|
||||
)
|
||||
|
||||
response->iCharCount = charCount;
|
||||
response->iSlotNum = 1;
|
||||
response->iPaymentFlag = 1;
|
||||
response->iOpenBetaFlag = 0;
|
||||
response->uiSvrTime = getTime();
|
||||
|
||||
// set username in response packet
|
||||
memcpy(response->szID, login->szID, sizeof(char16_t) * 33);
|
||||
|
||||
// update keys
|
||||
sock->setEKey(CNSocketEncryption::createNewKey(response->uiSvrTime, response->iCharCount + 1, response->iSlotNum + 1));
|
||||
sock->setFEKey(CNSocketEncryption::createNewKey((uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]), login->iClientVerC, 1));
|
||||
|
||||
// send the response in with original key
|
||||
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_LOGIN_SUCC, sizeof(sP_LS2CL_REP_LOGIN_SUCC), cachedKey));
|
||||
|
||||
loginSessions[sock] = CNLoginData();
|
||||
|
||||
if (settings::LOGINRANDCHARACTERS) {
|
||||
// now send the characters :)
|
||||
for (int i = 0; i < charCount; i++) {
|
||||
sP_LS2CL_REP_CHAR_INFO* charInfo = (sP_LS2CL_REP_CHAR_INFO*)xmalloc(sizeof(sP_LS2CL_REP_CHAR_INFO));
|
||||
charInfo->iSlot = (int8_t)i + 1;
|
||||
charInfo->iLevel = (int16_t)1;
|
||||
charInfo->sPC_Style.iPC_UID = rand(); // unique identifier for the character
|
||||
charInfo->sPC_Style.iNameCheck = 1;
|
||||
charInfo->sPC_Style.iGender = (i%2)+1; // can be 1(boy) or 2(girl)
|
||||
charInfo->sPC_Style.iFaceStyle = 1;
|
||||
charInfo->sPC_Style.iHairStyle = 1;
|
||||
charInfo->sPC_Style.iHairColor = (rand()%19) + 1; // 1 - 19
|
||||
charInfo->sPC_Style.iSkinColor = (rand()%13) + 1; // 1 - 13
|
||||
charInfo->sPC_Style.iEyeColor = (rand()%6) + 1; // 1 - 6
|
||||
charInfo->sPC_Style.iHeight = (rand()%6); // 0 -5
|
||||
charInfo->sPC_Style.iBody = (rand()%4); // 0 - 3
|
||||
charInfo->sPC_Style.iClass = 0;
|
||||
charInfo->sPC_Style2 = sPCStyle2(1, 1, 1);
|
||||
|
||||
// past's town hall
|
||||
charInfo->iX = settings::SPAWN_X;
|
||||
charInfo->iY = settings::SPAWN_Y;
|
||||
charInfo->iZ = settings::SPAWN_Z;
|
||||
|
||||
U8toU16(std::string("Player"), charInfo->sPC_Style.szFirstName);
|
||||
U8toU16(std::to_string(i + 1), charInfo->sPC_Style.szLastName);
|
||||
|
||||
int64_t UID = charInfo->sPC_Style.iPC_UID;
|
||||
loginSessions[sock].characters[UID] = Player();
|
||||
loginSessions[sock].characters[UID].level = charInfo->iLevel;
|
||||
loginSessions[sock].characters[UID].slot = charInfo->iSlot;
|
||||
loginSessions[sock].characters[UID].FEKey = sock->getFEKey();
|
||||
loginSessions[sock].characters[UID].x = charInfo->iX;
|
||||
loginSessions[sock].characters[UID].y = charInfo->iY;
|
||||
loginSessions[sock].characters[UID].z = charInfo->iZ;
|
||||
loginSessions[sock].characters[UID].PCStyle = charInfo->sPC_Style;
|
||||
loginSessions[sock].characters[UID].PCStyle2 = charInfo->sPC_Style2;
|
||||
|
||||
for (int i = 0; i < AEQUIP_COUNT; i++) {
|
||||
// setup item
|
||||
charInfo->aEquip[i].iID = 0;
|
||||
charInfo->aEquip[i].iType = i;
|
||||
charInfo->aEquip[i].iOpt = 0;
|
||||
loginSessions[sock].characters[UID].Equip[i] = charInfo->aEquip[i];
|
||||
}
|
||||
|
||||
// set default to the first character
|
||||
if (i == 0)
|
||||
loginSessions[sock].selectedChar = UID;
|
||||
|
||||
sock->sendPacket(new CNPacketData((void*)charInfo, P_LS2CL_REP_CHAR_INFO, sizeof(sP_LS2CL_REP_CHAR_INFO), sock->getEKey()));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case P_CL2LS_REP_LIVE_CHECK: {
|
||||
// stubbed, the client really doesn't care LOL
|
||||
break;
|
||||
}
|
||||
case P_CL2LS_REQ_CHECK_CHAR_NAME: {
|
||||
// naughty words allowed!!!!!!!! (also for some reason, the client will always show 'Player 0' if you manually type a name. It will show up for other connected players though)
|
||||
sP_CL2LS_REQ_CHECK_CHAR_NAME* nameCheck = (sP_CL2LS_REQ_CHECK_CHAR_NAME*)data->buf;
|
||||
sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC* response = (sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC));
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "P_CL2LS_REQ_CHECK_CHAR_NAME:" << std::endl;
|
||||
std::cout << "\tFirstName: " << U16toU8(nameCheck->szFirstName) << " LastName: " << U16toU8(nameCheck->szLastName) << std::endl;
|
||||
)
|
||||
|
||||
memcpy(response->szFirstName, nameCheck->szFirstName, sizeof(char16_t) * 9);
|
||||
memcpy(response->szLastName, nameCheck->szLastName, sizeof(char16_t) * 17);
|
||||
|
||||
// fr*ck allowed!!!
|
||||
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_CHECK_CHAR_NAME_SUCC, sizeof(sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC), sock->getEKey()));
|
||||
break;
|
||||
}
|
||||
case P_CL2LS_REQ_SAVE_CHAR_NAME: {
|
||||
sP_CL2LS_REQ_SAVE_CHAR_NAME* save = (sP_CL2LS_REQ_SAVE_CHAR_NAME*)data->buf;
|
||||
sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC* response = (sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC));
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "P_CL2LS_REQ_SAVE_CHAR_NAME:" << std::endl;
|
||||
std::cout << "\tSlot: " << (int)save->iSlotNum << std::endl;
|
||||
std::cout << "\tGender: " << (int)save->iGender << std::endl;
|
||||
std::cout << "\tName: " << U16toU8(save->szFirstName) << " " << U16toU8(save->szLastName) << std::endl;
|
||||
)
|
||||
|
||||
response->iSlotNum = save->iSlotNum;
|
||||
response->iGender = save->iGender;
|
||||
memcpy(response->szFirstName, save->szFirstName, sizeof(char16_t) * 9);
|
||||
memcpy(response->szLastName, save->szLastName, sizeof(char16_t) * 17);
|
||||
|
||||
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_SAVE_CHAR_NAME_SUCC, sizeof(sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC), sock->getEKey()));
|
||||
break;
|
||||
}
|
||||
case P_CL2LS_REQ_CHAR_CREATE: {
|
||||
sP_CL2LS_REQ_CHAR_CREATE* character = (sP_CL2LS_REQ_CHAR_CREATE*)data->buf;
|
||||
sP_LS2CL_REP_CHAR_CREATE_SUCC* response = (sP_LS2CL_REP_CHAR_CREATE_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_CHAR_CREATE_SUCC));
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "P_CL2LS_REQ_CHAR_CREATE:" << std::endl;
|
||||
std::cout << "\tPC_UID: " << character->PCStyle.iPC_UID << std::endl;
|
||||
std::cout << "\tNameCheck: " << (int)character->PCStyle.iNameCheck << std::endl;
|
||||
std::cout << "\tName: " << U16toU8(character->PCStyle.szFirstName) << " " << U16toU8(character->PCStyle.szLastName) << std::endl;
|
||||
std::cout << "\tGender: " << (int)character->PCStyle.iGender << std::endl;
|
||||
std::cout << "\tFace: " << (int)character->PCStyle.iFaceStyle << std::endl;
|
||||
std::cout << "\tHair: " << (int)character->PCStyle.iHairStyle << std::endl;
|
||||
std::cout << "\tHair Color: " << (int)character->PCStyle.iHairColor << std::endl;
|
||||
std::cout << "\tSkin Color: " << (int)character->PCStyle.iSkinColor << std::endl;
|
||||
std::cout << "\tEye Color: " << (int)character->PCStyle.iEyeColor << std::endl;
|
||||
std::cout << "\tHeight: " << (int)character->PCStyle.iHeight << std::endl;
|
||||
std::cout << "\tBody: " << (int)character->PCStyle.iBody << std::endl;
|
||||
std::cout << "\tClass: " << (int)character->PCStyle.iClass << std::endl;
|
||||
std::cout << "\tiEquipUBID: " << (int)character->sOn_Item.iEquipUBID << std::endl;
|
||||
std::cout << "\tiEquipLBID: " << (int)character->sOn_Item.iEquipLBID << std::endl;
|
||||
std::cout << "\tiEquipFootID: " << (int)character->sOn_Item.iEquipFootID << std::endl;
|
||||
)
|
||||
|
||||
character->PCStyle.iNameCheck = 1;
|
||||
response->PC_Style = character->PCStyle;
|
||||
response->PC_Style2 = sPCStyle2(1, 1, 1);
|
||||
response->iLevel = 1;
|
||||
response->sOn_Item = character->sOn_Item;
|
||||
|
||||
int64_t UID = character->PCStyle.iPC_UID;
|
||||
loginSessions[sock].characters[UID] = Player();
|
||||
loginSessions[sock].characters[UID].level = 1;
|
||||
loginSessions[sock].characters[UID].FEKey = sock->getFEKey();
|
||||
loginSessions[sock].characters[UID].PCStyle = character->PCStyle;
|
||||
loginSessions[sock].characters[UID].PCStyle2 = sPCStyle2(1, 0, 1);
|
||||
loginSessions[sock].characters[UID].x = settings::SPAWN_X;
|
||||
loginSessions[sock].characters[UID].y = settings::SPAWN_Y;
|
||||
loginSessions[sock].characters[UID].z = settings::SPAWN_Z;
|
||||
loginSessions[sock].characters[UID].Equip[1].iID = character->sOn_Item.iEquipUBID; // upper body
|
||||
loginSessions[sock].characters[UID].Equip[1].iType = 1;
|
||||
loginSessions[sock].characters[UID].Equip[2].iID = character->sOn_Item.iEquipLBID; // lower body
|
||||
loginSessions[sock].characters[UID].Equip[2].iType = 2;
|
||||
loginSessions[sock].characters[UID].Equip[3].iID = character->sOn_Item.iEquipFootID; // foot!
|
||||
loginSessions[sock].characters[UID].Equip[3].iType = 3;
|
||||
|
||||
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_CHAR_CREATE_SUCC, sizeof(sP_LS2CL_REP_CHAR_CREATE_SUCC), sock->getEKey()));
|
||||
break;
|
||||
}
|
||||
case P_CL2LS_REQ_CHAR_SELECT: {
|
||||
// character selected
|
||||
sP_CL2LS_REQ_CHAR_SELECT* chararacter = (sP_CL2LS_REQ_CHAR_SELECT*)data->buf;
|
||||
sP_LS2CL_REP_CHAR_SELECT_SUCC* response = (sP_LS2CL_REP_CHAR_SELECT_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_CHAR_SELECT_SUCC));
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "P_CL2LS_REQ_CHAR_SELECT:" << std::endl;
|
||||
std::cout << "\tPC_UID: " << chararacter->iPC_UID << std::endl;
|
||||
)
|
||||
|
||||
loginSessions[sock].selectedChar = chararacter->iPC_UID;
|
||||
|
||||
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_CHAR_SELECT_SUCC, sizeof(sP_LS2CL_REP_CHAR_SELECT_SUCC), sock->getEKey()));
|
||||
break;
|
||||
}
|
||||
case P_CL2LS_REQ_SHARD_SELECT: {
|
||||
// tell client to connect to the shard server
|
||||
sP_CL2LS_REQ_SHARD_SELECT* shard = (sP_CL2LS_REQ_SHARD_SELECT*)data->buf;
|
||||
sP_LS2CL_REP_SHARD_SELECT_SUCC* response = (sP_LS2CL_REP_SHARD_SELECT_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_SHARD_SELECT_SUCC));
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << "P_CL2LS_REQ_SHARD_SELECT:" << std::endl;
|
||||
std::cout << "\tShard: " << (int)shard->iShardNum << std::endl;
|
||||
)
|
||||
|
||||
const char* SHARD_IP = settings::SHARDSERVERIP.c_str();
|
||||
response->iEnterSerialKey = rand();
|
||||
response->g_FE_ServerPort = settings::SHARDPORT;
|
||||
|
||||
// copy IP to response (this struct uses ASCII encoding so we don't have to goof around converting encodings)
|
||||
memcpy(response->g_FE_ServerIP, SHARD_IP, strlen(SHARD_IP));
|
||||
response->g_FE_ServerIP[strlen(SHARD_IP)] = '\0';
|
||||
|
||||
// pass player to CNSharedData
|
||||
CNSharedData::setPlayer(response->iEnterSerialKey, loginSessions[sock].characters[loginSessions[sock].selectedChar]);
|
||||
|
||||
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_SHARD_SELECT_SUCC, sizeof(sP_LS2CL_REP_SHARD_SELECT_SUCC), sock->getEKey()));
|
||||
sock->kill(); // client should connect to the Shard server now
|
||||
break;
|
||||
}
|
||||
default:
|
||||
std::cerr << "OpenFusion: LOGIN UNIMPLM ERR. PacketType: " << data->type << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CNLoginServer::killConnection(CNSocket* cns) {
|
||||
loginSessions.erase(cns);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#ifndef _CNLS_HPP
|
||||
#define _CNLS_HPP
|
||||
|
||||
#include "CNProtocol.hpp"
|
||||
#include "Player.hpp"
|
||||
|
||||
#include <map>
|
||||
|
||||
enum LOGINPACKETID {
|
||||
// client to login server
|
||||
P_CL2LS_REQ_LOGIN = 301989889,
|
||||
P_CL2LS_REQ_CHECK_CHAR_NAME = 301989890,
|
||||
P_CL2LS_REQ_SAVE_CHAR_NAME = 301989891,
|
||||
P_CL2LS_REQ_CHAR_CREATE = 301989892,
|
||||
P_CL2LS_REQ_CHAR_SELECT = 301989893,
|
||||
P_CL2LS_REQ_SHARD_SELECT = 301989895,
|
||||
P_CL2LS_REP_LIVE_CHECK = 301989900,
|
||||
P_CL2LS_REQ_SHARD_LIST_INFO = 301989896,
|
||||
|
||||
// login server 2 client
|
||||
P_LS2CL_REP_LOGIN_SUCC = 553648129,
|
||||
P_LS2CL_REP_CHAR_INFO = 553648131,
|
||||
P_LS2CL_REP_CHECK_CHAR_NAME_SUCC = 553648133,
|
||||
P_LS2CL_REP_SAVE_CHAR_NAME_SUCC = 553648135,
|
||||
P_LS2CL_REP_CHAR_CREATE_SUCC = 553648137,
|
||||
P_LS2CL_REP_CHAR_SELECT_SUCC = 553648139,
|
||||
P_LS2CL_REP_SHARD_SELECT_SUCC = 553648143,
|
||||
P_LS2CL_REQ_LIVE_CHECK = 553648150,
|
||||
P_LS2CL_REP_SHARD_LIST_INFO_SUCC = 553648153
|
||||
};
|
||||
|
||||
struct CNLoginData {
|
||||
std::map<int64_t, Player> characters;
|
||||
int64_t selectedChar;
|
||||
};
|
||||
|
||||
// WARNING: THERE CAN ONLY BE ONE OF THESE SERVERS AT A TIME!!!!!! TODO: change loginSessions & packet handlers to be non-static
|
||||
class CNLoginServer : public CNServer {
|
||||
private:
|
||||
static void handlePacket(CNSocket* sock, CNPacketData* data);
|
||||
static std::map<CNSocket*, CNLoginData> loginSessions;
|
||||
|
||||
public:
|
||||
CNLoginServer(uint16_t p);
|
||||
|
||||
void killConnection(CNSocket* cns);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,283 +0,0 @@
|
||||
#include "CNProtocol.hpp"
|
||||
|
||||
// ========================================================[[ CNSocketEncryption ]]========================================================
|
||||
|
||||
// literally C/P from the client and converted to C++ (does some byte swapping /shrug)
|
||||
int CNSocketEncryption::Encrypt_byte_change_A(int ERSize, uint8_t* data, int size) {
|
||||
int num = 0;
|
||||
int num2 = 0;
|
||||
int num3 = 0;
|
||||
|
||||
while (num + ERSize <= size)
|
||||
{
|
||||
int num4 = num + num3;
|
||||
int num5 = num + (ERSize - 1 - num3);
|
||||
|
||||
uint8_t b = data[num4];
|
||||
data[num4] = data[num5];
|
||||
data[num5] = b;
|
||||
num += ERSize;
|
||||
num3++;
|
||||
if (num3 > ERSize / 2)
|
||||
{
|
||||
num3 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
num2 = ERSize - (num + ERSize - size);
|
||||
return num + num2;
|
||||
}
|
||||
|
||||
int CNSocketEncryption::xorData(uint8_t* buffer, uint8_t* key, int size) {
|
||||
// xor every 8 bytes with 8 byte key
|
||||
for (int i = 0; i < size; i++) {
|
||||
buffer[i] ^= key[i % keyLength];
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
uint64_t CNSocketEncryption::createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2) {
|
||||
uint64_t num = (uint64_t)(iv1 + 1);
|
||||
uint64_t num2 = (uint64_t)(iv2 + 1);
|
||||
uint64_t dEKey = (uint64_t)(*(uint64_t*)&defaultKey[0]);
|
||||
return dEKey * (uTime * num * num2);
|
||||
}
|
||||
|
||||
int CNSocketEncryption::encryptData(uint8_t* buffer, uint8_t* key, int size) {
|
||||
int eRSize = size % (keyLength / 2 + 1) * 2 + keyLength; // C/P from client
|
||||
int size2 = xorData(buffer, key, size);
|
||||
return Encrypt_byte_change_A(eRSize, buffer, size2);
|
||||
}
|
||||
|
||||
int CNSocketEncryption::decryptData(uint8_t* buffer, uint8_t* key, int size) {
|
||||
int eRSize = size % (keyLength / 2 + 1) * 2 + keyLength; // size % of 18????
|
||||
int size2 = Encrypt_byte_change_A(eRSize, buffer, size);
|
||||
return xorData(buffer, key, size2);
|
||||
}
|
||||
|
||||
// ========================================================[[ CNPacketData ]]========================================================
|
||||
|
||||
CNPacketData::CNPacketData(void* b, uint32_t t, int l, uint64_t k): buf(b), type(t), size(l), key(k) {}
|
||||
|
||||
CNPacketData::~CNPacketData() {
|
||||
free(buf); // we own the buffer
|
||||
}
|
||||
|
||||
// ========================================================[[ CNSocket ]]========================================================
|
||||
|
||||
CNSocket::CNSocket(SOCKET s, PacketHandler ph): sock(s), pHandler(ph) {
|
||||
EKey = (uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]);
|
||||
}
|
||||
|
||||
bool CNSocket::sendData(uint8_t* data, int size) {
|
||||
int sentBytes = 0;
|
||||
|
||||
while (sentBytes < size) {
|
||||
int sent = send(sock, (buffer_t*)(data + sentBytes), size - sentBytes, 0); // no flags defined
|
||||
if (SOCKETERROR(sent))
|
||||
return false; // error occured while sending bytes
|
||||
sentBytes += sent;
|
||||
}
|
||||
|
||||
return true; // it worked!
|
||||
}
|
||||
|
||||
void CNSocket::setEKey(uint64_t k) {
|
||||
EKey = k;
|
||||
}
|
||||
|
||||
void CNSocket::setFEKey(uint64_t k) {
|
||||
FEKey = k;
|
||||
}
|
||||
|
||||
uint64_t CNSocket::getEKey() {
|
||||
return EKey;
|
||||
}
|
||||
|
||||
uint64_t CNSocket::getFEKey() {
|
||||
return FEKey;
|
||||
}
|
||||
|
||||
bool CNSocket::isAlive() {
|
||||
return alive;
|
||||
}
|
||||
|
||||
void CNSocket::kill() {
|
||||
alive = false;
|
||||
#ifdef _WIN32
|
||||
shutdown(sock, SD_BOTH);
|
||||
closesocket(sock);
|
||||
#else
|
||||
shutdown(sock, SHUT_RDWR);
|
||||
close(sock);
|
||||
#endif
|
||||
}
|
||||
|
||||
void CNSocket::sendPacket(CNPacketData* pak) {
|
||||
int tmpSize = pak->size + sizeof(uint32_t);
|
||||
uint8_t* tmpBuf = (uint8_t*)xmalloc(tmpSize);
|
||||
|
||||
// copy packet type to the front of the buffer & then the actual buffer
|
||||
memcpy(tmpBuf, (void*)&pak->type, sizeof(uint32_t));
|
||||
memcpy(tmpBuf+sizeof(uint32_t), pak->buf, pak->size);
|
||||
|
||||
// encrypt the packet
|
||||
CNSocketEncryption::encryptData((uint8_t*)tmpBuf, (uint8_t*)(&pak->key), tmpSize);
|
||||
|
||||
// send packet size
|
||||
sendData((uint8_t*)&tmpSize, sizeof(uint32_t));
|
||||
|
||||
// send packet data!
|
||||
sendData(tmpBuf, tmpSize);
|
||||
|
||||
delete pak;
|
||||
free(tmpBuf); // free tmp buffer
|
||||
}
|
||||
|
||||
void CNSocket::step() {
|
||||
if (readSize <= 0) {
|
||||
// we aren't reading a packet yet, try to start looking for one
|
||||
int recved = recv(sock, (buffer_t*)readBuffer, sizeof(int32_t), 0);
|
||||
if (!SOCKETERROR(recved)) {
|
||||
// we got out packet size!!!!
|
||||
readSize = *((int32_t*)readBuffer);
|
||||
// sanity check
|
||||
if (readSize > MAX_PACKETSIZE) {
|
||||
kill();
|
||||
return;
|
||||
}
|
||||
|
||||
// we'll just leave bufferIndex at 0 since we already have the packet size, it's safe to overwrite those bytes
|
||||
activelyReading = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (readSize > 0 && readBufferIndex < readSize) {
|
||||
// read until the end of the packet! (or at least try too)
|
||||
int recved = recv(sock, (buffer_t*)(readBuffer + readBufferIndex), readSize - readBufferIndex, 0);
|
||||
if (!SOCKETERROR(recved))
|
||||
readBufferIndex += recved;
|
||||
}
|
||||
|
||||
if (activelyReading && readBufferIndex - readSize <= 0) {
|
||||
// decrypt readBuffer and copy to CNPacketData
|
||||
CNSocketEncryption::decryptData(readBuffer, (uint8_t*)(&EKey), readSize);
|
||||
|
||||
// this doesn't leak memory because we free it in CNPacketData's deconstructor LOL
|
||||
void* tmpBuf = xmalloc(readSize-sizeof(int32_t));
|
||||
memcpy(tmpBuf, readBuffer+sizeof(uint32_t), readSize-sizeof(int32_t));
|
||||
CNPacketData tmp(tmpBuf, *((uint32_t*)readBuffer), readSize-sizeof(int32_t), EKey);
|
||||
|
||||
// CALL PACKET HANDLER!!
|
||||
pHandler(this, &tmp); // tmp's deconstructor will be called when readStep returns so that tmpBuffer we made will be cleaned up :)
|
||||
|
||||
// reset vars :)
|
||||
readSize = 0;
|
||||
readBufferIndex = 0;
|
||||
activelyReading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================[[ CNServer ]]========================================================
|
||||
|
||||
void CNServer::init() {
|
||||
// create socket file descriptor
|
||||
sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (SOCKETINVALID(sock)) {
|
||||
std::cerr << "[FATAL] OpenFusion: socket failed" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// attach socket to the port
|
||||
int opt = 1;
|
||||
#ifdef _WIN32
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)) != 0) {
|
||||
#else
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) {
|
||||
#endif
|
||||
std::cerr << "[FATAL] OpenFusion: setsockopt failed" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
address.sin_port = htons(port);
|
||||
|
||||
addressSize = sizeof(address);
|
||||
|
||||
// Bind to the port
|
||||
if (SOCKETERROR(bind(sock, (struct sockaddr *)&address, addressSize))) {
|
||||
std::cerr << "[FATAL] OpenFusion: bind failed" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (SOCKETERROR(listen(sock, SOMAXCONN))) {
|
||||
std::cerr << "[FATAL] OpenFusion: listen failed" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// set server listener to non-blocking
|
||||
#ifdef _WIN32
|
||||
unsigned long mode = 1;
|
||||
if (ioctlsocket(sock, FIONBIO, &mode) != 0) {
|
||||
#else
|
||||
if (fcntl(sock, F_SETFL, (fcntl(sock, F_GETFL, 0) | O_NONBLOCK)) != 0) {
|
||||
#endif
|
||||
std::cerr << "[FATAL] OpenFusion: fcntl failed" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
CNServer::CNServer() {};
|
||||
CNServer::CNServer(uint16_t p): port(p) {}
|
||||
|
||||
void CNServer::start() {
|
||||
std::cout << "Starting server at *:" << port << std::endl;
|
||||
// listen to new connections, add to connection list
|
||||
while (true) {
|
||||
// listen for a new connection
|
||||
SOCKET newConnection = accept(sock, (struct sockaddr *)&(address), (socklen_t*)&(addressSize));
|
||||
if (!SOCKETINVALID(newConnection)) {
|
||||
// new connection! make sure to set non-blocking!
|
||||
#ifdef _WIN32
|
||||
unsigned long mode = 1;
|
||||
if (ioctlsocket(newConnection, FIONBIO, &mode) != 0) {
|
||||
#else
|
||||
if (fcntl(newConnection, F_SETFL, (fcntl(sock, F_GETFL, 0) | O_NONBLOCK)) != 0) {
|
||||
#endif
|
||||
std::cerr << "[FATAL] OpenFusion: fcntl failed on new connection" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
std::cout << "New connection! " << inet_ntoa(address.sin_addr) << std::endl;
|
||||
|
||||
// add connection to list!
|
||||
CNSocket* tmp = new CNSocket(newConnection, pHandler);
|
||||
connections.push_back(tmp);
|
||||
}
|
||||
|
||||
// for each connection, check if it's alive, if not kill it!
|
||||
std::list<CNSocket*>::iterator i = connections.begin();
|
||||
while (i != connections.end()) {
|
||||
CNSocket* cSock = *i;
|
||||
|
||||
if (cSock->isAlive()) {
|
||||
cSock->step();
|
||||
|
||||
++i; // go to the next element
|
||||
} else {
|
||||
killConnection(cSock);
|
||||
connections.erase(i++);
|
||||
delete cSock;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
Sleep(0);
|
||||
#else
|
||||
sleep(0); // so your cpu isn't at 100% all the time, we don't need all of that! im not hacky! you're hacky!
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void CNServer::killConnection(CNSocket* cns) {} // stubbed lol
|
||||
@@ -1,138 +0,0 @@
|
||||
#ifndef _CNP_HPP
|
||||
#define _CNP_HPP
|
||||
|
||||
#define MAX_PACKETSIZE 8192
|
||||
#define DEBUGLOG(x) x
|
||||
|
||||
#include <iostream>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#ifdef _WIN32
|
||||
// windows (UNTESTED)
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <ws2tcpip.h>
|
||||
#pragma comment(lib, "Ws2_32.lib")
|
||||
|
||||
typedef char buffer_t;
|
||||
//#define errno WSAGetLastError()
|
||||
#define SOCKETINVALID(x) (x == INVALID_SOCKET)
|
||||
#define SOCKETERROR(x) (x == SOCKET_ERROR)
|
||||
#else
|
||||
// posix platform
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
typedef int SOCKET;
|
||||
typedef void buffer_t;
|
||||
#define SOCKETINVALID(x) (x < 0)
|
||||
#define SOCKETERROR(x) (x == -1)
|
||||
#endif
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <csignal>
|
||||
#include <list>
|
||||
#include <queue>
|
||||
|
||||
/*
|
||||
Packets format (sent from the client):
|
||||
[4 bytes] - size of packet (including these 4 bytes!)
|
||||
[size bytes] - Encrypted packet (byte swapped && xor'd with 8 byte key; see CNSocketEncryption)
|
||||
[4 bytes] - packet type (which is a combination of the first 4 bytes of the packet and a checksum in some versions)
|
||||
[structure]
|
||||
*/
|
||||
|
||||
// error checking calloc wrapper
|
||||
inline void* xmalloc(size_t sz) {
|
||||
void* res = calloc(1, sz);
|
||||
|
||||
if (res == NULL) {
|
||||
std::cerr << "[FATAL] OpenFusion: calloc failed to allocate memory!" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
namespace CNSocketEncryption {
|
||||
// you won't believe how complicated they made it in the client :facepalm:
|
||||
static constexpr const char* defaultKey = "m@rQn~W#";
|
||||
static const unsigned int keyLength = 8;
|
||||
|
||||
int Encrypt_byte_change_A(int ERSize, uint8_t* data, int size);
|
||||
int xorData(uint8_t* buffer, uint8_t* key, int size);
|
||||
uint64_t createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2);
|
||||
int encryptData(uint8_t* buffer, uint8_t* key, int size);
|
||||
int decryptData(uint8_t* buffer, uint8_t* key, int size);
|
||||
}
|
||||
|
||||
class CNPacketData {
|
||||
public:
|
||||
void* buf;
|
||||
int size;
|
||||
uint32_t type;
|
||||
uint64_t key;
|
||||
|
||||
CNPacketData(void* b, uint32_t t, int l, uint64_t k);
|
||||
~CNPacketData();
|
||||
};
|
||||
|
||||
class CNSocket;
|
||||
typedef void (*PacketHandler)(CNSocket* sock, CNPacketData* data);
|
||||
|
||||
class CNSocket {
|
||||
private:
|
||||
uint64_t EKey;
|
||||
uint64_t FEKey;
|
||||
int32_t readSize = 0;
|
||||
uint8_t* readBuffer = new uint8_t[MAX_PACKETSIZE];
|
||||
int readBufferIndex = 0;
|
||||
bool activelyReading = false;
|
||||
bool alive = true;
|
||||
|
||||
bool sendData(uint8_t* data, int size);
|
||||
|
||||
public:
|
||||
SOCKET sock;
|
||||
PacketHandler pHandler;
|
||||
|
||||
CNSocket(SOCKET s, PacketHandler ph);
|
||||
|
||||
void setEKey(uint64_t k);
|
||||
void setFEKey(uint64_t k);
|
||||
uint64_t getEKey();
|
||||
uint64_t getFEKey();
|
||||
|
||||
void kill();
|
||||
void sendPacket(CNPacketData* pak);
|
||||
void step();
|
||||
bool isAlive();
|
||||
};
|
||||
|
||||
// in charge of accepting new connections and making sure each connection is kept alive
|
||||
class CNServer {
|
||||
protected:
|
||||
std::list<CNSocket*> connections;
|
||||
|
||||
SOCKET sock;
|
||||
uint16_t port;
|
||||
socklen_t addressSize;
|
||||
struct sockaddr_in address;
|
||||
void init();
|
||||
|
||||
public:
|
||||
PacketHandler pHandler;
|
||||
|
||||
CNServer();
|
||||
CNServer(uint16_t p);
|
||||
|
||||
void start();
|
||||
virtual void killConnection(CNSocket* cns);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,33 +0,0 @@
|
||||
#include "CNProtocol.hpp"
|
||||
#include "CNStructs.hpp"
|
||||
#include "CNShardServer.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "CNShared.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <cstdlib>
|
||||
|
||||
std::map<uint32_t, PacketHandler> CNShardServer::ShardPackets;
|
||||
|
||||
CNShardServer::CNShardServer(uint16_t p) {
|
||||
port = p;
|
||||
pHandler = &CNShardServer::handlePacket;
|
||||
init();
|
||||
}
|
||||
|
||||
void CNShardServer::handlePacket(CNSocket* sock, CNPacketData* data) {
|
||||
if (ShardPackets.find(data->type) != ShardPackets.end())
|
||||
ShardPackets[data->type](sock, data);
|
||||
else
|
||||
std::cerr << "OpenFusion: SHARD UNIMPLM ERR. PacketType: " << data->type << std::endl;
|
||||
}
|
||||
|
||||
void CNShardServer::killConnection(CNSocket* cns) {
|
||||
// remove from CNSharedData
|
||||
Player cachedPlr = PlayerManager::getPlayer(cns);
|
||||
PlayerManager::removePlayer(cns);
|
||||
|
||||
CNSharedData::erasePlayer(cachedPlr.SerialKey);
|
||||
std::cout << U16toU8(cachedPlr.PCStyle.szFirstName) << " " << U16toU8(cachedPlr.PCStyle.szLastName) << " left" << std::endl;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#ifndef _CNSS_HPP
|
||||
#define _CNSS_HPP
|
||||
|
||||
#include "CNProtocol.hpp"
|
||||
|
||||
#include <map>
|
||||
|
||||
enum SHARDPACKETID {
|
||||
// client 2 shard
|
||||
P_CL2FE_REQ_PC_ENTER = 318767105,
|
||||
P_CL2FE_REQ_PC_LOADING_COMPLETE = 318767245,
|
||||
P_CL2FE_REQ_PC_MOVE = 318767107,
|
||||
P_CL2FE_REQ_PC_STOP = 318767108,
|
||||
P_CL2FE_REQ_PC_JUMP = 318767109,
|
||||
P_CL2FE_REQ_PC_MOVEPLATFORM = 318767168,
|
||||
P_CL2FE_REQ_PC_GOTO = 318767124,
|
||||
P_CL2FE_GM_REQ_PC_SET_VALUE = 318767211,
|
||||
P_CL2FE_REQ_SEND_FREECHAT_MESSAGE = 318767111,
|
||||
P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT = 318767184,
|
||||
|
||||
// shard 2 client
|
||||
P_FE2CL_REP_PC_ENTER_SUCC = 822083586,
|
||||
P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC = 822083833,
|
||||
P_FE2CL_PC_NEW = 822083587,
|
||||
P_FE2CL_PC_MOVE = 822083592,
|
||||
P_FE2CL_PC_STOP = 822083593,
|
||||
P_FE2CL_PC_JUMP = 822083594,
|
||||
P_FE2CL_PC_EXIT = 822083590,
|
||||
P_FE2CL_PC_MOVEPLATFORM = 822083704,
|
||||
P_FE2CL_REP_PC_GOTO_SUCC = 822083633,
|
||||
P_FE2CL_GM_REP_PC_SET_VALUE = 822083781,
|
||||
P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT = 822083730
|
||||
};
|
||||
|
||||
#define REGISTER_SHARD_PACKET(pactype, handlr) CNShardServer::ShardPackets[pactype] = handlr;
|
||||
|
||||
// WARNING: THERE CAN ONLY BE ONE OF THESE SERVERS AT A TIME!!!!!! TODO: change players & packet handlers to be non-static
|
||||
class CNShardServer : public CNServer {
|
||||
private:
|
||||
static void handlePacket(CNSocket* sock, CNPacketData* data);
|
||||
public:
|
||||
static std::map<uint32_t, PacketHandler> ShardPackets;
|
||||
|
||||
CNShardServer(uint16_t p);
|
||||
|
||||
void killConnection(CNSocket* cns);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,25 +0,0 @@
|
||||
#include "CNShared.hpp"
|
||||
|
||||
#ifdef __MINGW32__
|
||||
#include "mingw/mingw.mutex.h"
|
||||
#else
|
||||
#include <mutex>
|
||||
#endif
|
||||
std::map<int64_t, Player> CNSharedData::players;
|
||||
std::mutex playerCrit;
|
||||
|
||||
void CNSharedData::setPlayer(int64_t sk, Player& plr) {
|
||||
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
|
||||
|
||||
players[sk] = plr;
|
||||
}
|
||||
|
||||
Player CNSharedData::getPlayer(int64_t sk) {
|
||||
return players[sk];
|
||||
}
|
||||
|
||||
void CNSharedData::erasePlayer(int64_t sk) {
|
||||
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
|
||||
|
||||
players.erase(sk);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
CNShared.hpp
|
||||
There's some data shared between the Login Server and the Shard Server. Of course all of this needs to be thread-safe. No mucking about on this one!
|
||||
*/
|
||||
|
||||
#ifndef _CNSD_HPP
|
||||
#define _CNSD_HPP
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "Player.hpp"
|
||||
|
||||
namespace CNSharedData {
|
||||
// serialkey corresponds to player data
|
||||
extern std::map<int64_t, Player> players;
|
||||
|
||||
void setPlayer(int64_t sk, Player& plr);
|
||||
Player getPlayer(int64_t sk);
|
||||
void erasePlayer(int64_t sk);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,24 +0,0 @@
|
||||
#include "CNStructs.hpp"
|
||||
|
||||
std::string U16toU8(char16_t* src) {
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
|
||||
return convert.to_bytes(src);
|
||||
}
|
||||
|
||||
// returns number of char16_t that was written at des
|
||||
int U8toU16(std::string src, char16_t* des) {
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
|
||||
std::u16string tmp = convert.from_bytes(src);
|
||||
|
||||
// copy utf16 string to buffer
|
||||
memcpy(des, tmp.c_str(), sizeof(char16_t) * tmp.length());
|
||||
des[tmp.length()] = '\0';
|
||||
|
||||
return tmp.length();
|
||||
}
|
||||
|
||||
uint64_t getTime() {
|
||||
struct timeval tp;
|
||||
gettimeofday(&tp, NULL);
|
||||
return tp.tv_sec * 1000 + tp.tv_usec / 1000;
|
||||
}
|
||||
@@ -1,533 +0,0 @@
|
||||
/*
|
||||
CNStructs.hpp - defines some basic structs & useful methods for packets used by FusionFall
|
||||
|
||||
NOTE: this is missing the vast majority of packets, I have also ommitted the ERR & FAIL packets for simplicity
|
||||
*/
|
||||
|
||||
#ifndef _CNS_HPP
|
||||
#define _CNS_HPP
|
||||
|
||||
#include <iostream>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/time.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <locale>
|
||||
#include <codecvt>
|
||||
|
||||
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt
|
||||
|
||||
std::string U16toU8(char16_t* src);
|
||||
|
||||
// returns number of char16_t that was written at des
|
||||
int U8toU16(std::string src, char16_t* des);
|
||||
|
||||
uint64_t getTime();
|
||||
|
||||
//#define CNPROTO_VERSION_0728
|
||||
|
||||
#ifdef CNPROTO_VERSION_0728
|
||||
#define AEQUIP_COUNT 12
|
||||
#else
|
||||
#define AEQUIP_COUNT 9
|
||||
#endif
|
||||
|
||||
// ========================================================[[ General Purpose ]]========================================================
|
||||
|
||||
// sets the same byte alignment as the structs in the client
|
||||
#pragma pack(push, 4)
|
||||
struct sPCStyle {
|
||||
int64_t iPC_UID;
|
||||
int8_t iNameCheck;
|
||||
|
||||
char16_t szFirstName[9];
|
||||
char16_t szLastName[17];
|
||||
|
||||
int8_t iGender;
|
||||
int8_t iFaceStyle;
|
||||
int8_t iHairStyle;
|
||||
int8_t iHairColor;
|
||||
int8_t iSkinColor;
|
||||
int8_t iEyeColor;
|
||||
int8_t iHeight;
|
||||
int8_t iBody;
|
||||
int32_t iClass;
|
||||
};
|
||||
|
||||
#pragma pack(1)
|
||||
struct sPCStyle2 {
|
||||
int8_t iAppearanceFlag;
|
||||
int8_t iTutorialFlag;
|
||||
int8_t iPayzoneFlag;
|
||||
|
||||
sPCStyle2() {}
|
||||
sPCStyle2(int8_t a, int8_t t, int8_t p):
|
||||
iAppearanceFlag(a), iTutorialFlag(t), iPayzoneFlag(p) {}
|
||||
};
|
||||
|
||||
#pragma pack(2)
|
||||
struct sOnItem {
|
||||
int16_t iEquipHandID;
|
||||
int16_t iEquipUBID;
|
||||
int16_t iEquipLBID;
|
||||
int16_t iEquipFootID;
|
||||
int16_t iEquipHeadID;
|
||||
int16_t iEquipFaceID;
|
||||
int16_t iEquipBackID;
|
||||
};
|
||||
|
||||
struct sOnItem_Index {
|
||||
int16_t iEquipUBID_index;
|
||||
int16_t iEquipLBID_index;
|
||||
int16_t iEquipFootID_index;
|
||||
int16_t iFaceStyle;
|
||||
int16_t iHairStyle;
|
||||
};
|
||||
|
||||
struct sNano {
|
||||
int16_t iID;
|
||||
int16_t iSkillID;
|
||||
int16_t iStamina;
|
||||
};
|
||||
|
||||
#pragma pack(4)
|
||||
struct sItemBase {
|
||||
int16_t iType;
|
||||
int16_t iID;
|
||||
int32_t iOpt;
|
||||
int32_t iTimeLimit;
|
||||
|
||||
#ifdef CNPROTO_VERSION_0728
|
||||
int32_t iSerial;
|
||||
#endif
|
||||
};
|
||||
|
||||
struct sTimeBuff {
|
||||
uint64_t iTimeLimit;
|
||||
uint64_t iTimeDuration;
|
||||
|
||||
int32_t iTimeRepeat;
|
||||
int32_t iValue;
|
||||
int32_t iConfirmNum;
|
||||
};
|
||||
|
||||
struct sRunningQuest {
|
||||
int32_t m_aCurrTaskID;
|
||||
|
||||
int32_t m_aKillNPCID[3];
|
||||
int32_t m_aKillNPCCount[3];
|
||||
int32_t m_aNeededItemID[3];
|
||||
int32_t m_aNeededItemCount[3];
|
||||
};
|
||||
|
||||
struct sPCLoadData2CL {
|
||||
int16_t iUserLevel;
|
||||
sPCStyle PCStyle;
|
||||
sPCStyle2 PCStyle2;
|
||||
|
||||
int16_t iLevel;
|
||||
int16_t iMentor;
|
||||
int16_t iMentorCount;
|
||||
int32_t iHP;
|
||||
int32_t iBatteryW;
|
||||
int32_t iBatteryN;
|
||||
int32_t iCandy;
|
||||
int32_t iFusionMatter;
|
||||
int8_t iSpecialState;
|
||||
int32_t iMapNum;
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
int32_t iAngle;
|
||||
|
||||
sItemBase aEquip[AEQUIP_COUNT];
|
||||
sItemBase aInven[50];
|
||||
sItemBase aQInven[50];
|
||||
|
||||
sNano aNanoBank[37];
|
||||
|
||||
int16_t aNanoSlots[3];
|
||||
|
||||
int16_t iActiveNanoSlotNum;
|
||||
int32_t iConditionBitFlag;
|
||||
int32_t eCSTB___Add;
|
||||
|
||||
sTimeBuff TimeBuff;
|
||||
|
||||
int64_t aQuestFlag[32];
|
||||
int64_t aRepeatQuestFlag[8];
|
||||
|
||||
sRunningQuest aRunningQuest[9];
|
||||
|
||||
int32_t iCurrentMissionID;
|
||||
int32_t iWarpLocationFlag;
|
||||
|
||||
int64_t aWyvernLocationFlag[2];
|
||||
|
||||
int32_t iBuddyWarpTime;
|
||||
int32_t iFatigue;
|
||||
int32_t iFatigue_Level;
|
||||
int32_t iFatigueRate;
|
||||
int64_t iFirstUseFlag1;
|
||||
int64_t iFirstUseFlag2;
|
||||
|
||||
int32_t aiPCSkill[33];
|
||||
|
||||
sPCLoadData2CL() {};
|
||||
};
|
||||
|
||||
struct sPCAppearanceData {
|
||||
int32_t iID;
|
||||
sPCStyle PCStyle;
|
||||
|
||||
int32_t iConditionBitFlag;
|
||||
int8_t iPCState;
|
||||
int8_t iSpecialState;
|
||||
int16_t iLv;
|
||||
int32_t iHP;
|
||||
|
||||
int32_t iMapNum;
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
int32_t iAngle;
|
||||
|
||||
sItemBase ItemEquip[AEQUIP_COUNT];
|
||||
sNano Nano;
|
||||
|
||||
int32_t eRT;
|
||||
};
|
||||
|
||||
// ========================================================[[ Client2LoginServer packets ]]========================================================
|
||||
|
||||
#pragma pack(4)
|
||||
struct sP_CL2LS_REQ_LOGIN {
|
||||
char16_t szID[33];
|
||||
char16_t szPassword[33];
|
||||
|
||||
int32_t iClientVerA;
|
||||
int32_t iClientVerB;
|
||||
int32_t iClientVerC;
|
||||
|
||||
int32_t iLoginType;
|
||||
|
||||
uint8_t szCookie_TEGid[64];
|
||||
uint8_t szCookie_authid[255];
|
||||
};
|
||||
|
||||
struct sP_CL2LS_REQ_CHECK_CHAR_NAME {
|
||||
int32_t iFNCode;
|
||||
int32_t iLNCode;
|
||||
int32_t iMNCode;
|
||||
|
||||
char16_t szFirstName[9];
|
||||
char16_t szLastName[17];
|
||||
};
|
||||
|
||||
struct sP_CL2LS_REQ_SAVE_CHAR_NAME {
|
||||
int8_t iSlotNum;
|
||||
int8_t iGender;
|
||||
int32_t iFNCode;
|
||||
int32_t iLNCode;
|
||||
int32_t iMNCode;
|
||||
|
||||
char16_t szFirstName[9];
|
||||
char16_t szLastName[17];
|
||||
};
|
||||
|
||||
struct sP_CL2LS_REQ_CHAR_CREATE {
|
||||
sPCStyle PCStyle;
|
||||
sOnItem sOn_Item;
|
||||
sOnItem_Index sOn_Item_Index;
|
||||
};
|
||||
|
||||
struct sP_CL2LS_REQ_CHAR_SELECT {
|
||||
int64_t iPC_UID;
|
||||
};
|
||||
|
||||
struct sP_CL2LS_REP_LIVE_CHECK {
|
||||
int32_t unused;
|
||||
};
|
||||
|
||||
#pragma pack(1)
|
||||
struct sP_CL2LS_REQ_SHARD_SELECT {
|
||||
int8_t iShardNum;
|
||||
};
|
||||
|
||||
struct sP_CL2LS_REQ_SHARD_LIST_INFO {
|
||||
uint8_t unused;
|
||||
};
|
||||
|
||||
// ========================================================[[ LoginServer2Client packets ]]========================================================
|
||||
|
||||
#pragma pack(4)
|
||||
struct sP_LS2CL_REP_LOGIN_SUCC {
|
||||
int8_t iCharCount;
|
||||
int8_t iSlotNum;
|
||||
int8_t iPaymentFlag;
|
||||
int8_t iTempForPacking4; // UNUSED
|
||||
uint64_t uiSvrTime; // UNIX timestamp
|
||||
|
||||
char16_t szID[33];
|
||||
|
||||
uint32_t iOpenBetaFlag;
|
||||
};
|
||||
|
||||
#pragma pack(2)
|
||||
struct sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC {
|
||||
char16_t szFirstName[9];
|
||||
char16_t szLastName[17];
|
||||
};
|
||||
|
||||
#pragma pack(4)
|
||||
struct sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC {
|
||||
int64_t iPC_UID;
|
||||
int8_t iSlotNum;
|
||||
int8_t iGender;
|
||||
|
||||
char16_t szFirstName[9];
|
||||
char16_t szLastName[17];
|
||||
};
|
||||
|
||||
struct sP_LS2CL_REP_CHAR_CREATE_SUCC {
|
||||
int16_t iLevel;
|
||||
sPCStyle PC_Style;
|
||||
sPCStyle2 PC_Style2;
|
||||
sOnItem sOn_Item;
|
||||
};
|
||||
|
||||
struct sP_LS2CL_REP_CHAR_INFO {
|
||||
int8_t iSlot;
|
||||
int16_t iLevel;
|
||||
|
||||
sPCStyle sPC_Style;
|
||||
sPCStyle2 sPC_Style2;
|
||||
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
|
||||
sItemBase aEquip[AEQUIP_COUNT];
|
||||
};
|
||||
|
||||
struct sP_LS2CL_REP_SHARD_SELECT_SUCC {
|
||||
uint8_t g_FE_ServerIP[16]; // Ascii
|
||||
int32_t g_FE_ServerPort;
|
||||
int64_t iEnterSerialKey;
|
||||
};
|
||||
|
||||
#pragma pack(1)
|
||||
struct sP_LS2CL_REP_CHAR_SELECT_SUCC {
|
||||
int8_t unused;
|
||||
};
|
||||
|
||||
struct sP_LS2CL_REP_SHARD_LIST_INFO_SUCC {
|
||||
uint8_t aShardConnectFlag[27];
|
||||
};
|
||||
|
||||
|
||||
// ========================================================[[ Client2ShardServer packets ]]========================================================
|
||||
|
||||
#pragma pack(4)
|
||||
struct sP_CL2FE_REQ_PC_ENTER {
|
||||
char16_t szID[33];
|
||||
int32_t iTempValue;
|
||||
int64_t iEnterSerialKey;
|
||||
};
|
||||
|
||||
|
||||
struct sP_CL2FE_REQ_PC_LOADING_COMPLETE {
|
||||
int32_t iPC_ID;
|
||||
};
|
||||
|
||||
struct sP_CL2FE_REQ_PC_MOVE {
|
||||
uint64_t iCliTime;
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
float fVX;
|
||||
float fVY;
|
||||
float fVZ;
|
||||
int32_t iAngle;
|
||||
uint8_t cKeyValue;
|
||||
int32_t iSpeed;
|
||||
};
|
||||
|
||||
struct sP_CL2FE_REQ_PC_STOP {
|
||||
uint64_t iCliTime;
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
};
|
||||
|
||||
struct sP_CL2FE_REQ_PC_JUMP {
|
||||
uint64_t iCliTime;
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
int32_t iVX;
|
||||
int32_t iVY;
|
||||
int32_t iVZ;
|
||||
int32_t iAngle;
|
||||
uint8_t cKeyValue;
|
||||
int32_t iSpeed;
|
||||
};
|
||||
|
||||
struct sP_CL2FE_REQ_PC_MOVEPLATFORM {
|
||||
uint64_t iCliTime;
|
||||
|
||||
int32_t iLcX;
|
||||
int32_t iLcY;
|
||||
int32_t iLcZ;
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
|
||||
float fVX;
|
||||
float fVY;
|
||||
float fVZ;
|
||||
|
||||
int32_t bDown;
|
||||
uint32_t iPlatformID;
|
||||
int32_t iAngle;
|
||||
uint32_t cKeyValue;
|
||||
int32_t iSpeed;
|
||||
};
|
||||
|
||||
struct sP_CL2FE_REQ_PC_GOTO {
|
||||
int32_t iToX;
|
||||
int32_t iToY;
|
||||
int32_t iToZ;
|
||||
};
|
||||
|
||||
struct sP_CL2FE_GM_REQ_PC_SET_VALUE {
|
||||
int32_t iPC_ID;
|
||||
int32_t iSetValueType;
|
||||
int32_t iSetValue;
|
||||
};
|
||||
|
||||
struct sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE {
|
||||
char16_t szFreeChat[128];
|
||||
int32_t iEmoteCode;
|
||||
};
|
||||
|
||||
struct sP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT {
|
||||
int32_t iID_From;
|
||||
int32_t iEmoteCode;
|
||||
};
|
||||
|
||||
// ========================================================[[ ShardServer2Client packets ]]========================================================
|
||||
|
||||
struct sP_FE2CL_REP_PC_ENTER_SUCC {
|
||||
int32_t iID;
|
||||
sPCLoadData2CL PCLoadData2CL;
|
||||
uint64_t uiSvrTime;
|
||||
};
|
||||
|
||||
struct sP_FE2CL_REP_PC_LOADING_COMPLETE_SUCC {
|
||||
int32_t iPC_ID;
|
||||
};
|
||||
|
||||
// literally just a wrapper for a sPCAppearanceData struct :/
|
||||
struct sP_FE2CL_PC_NEW {
|
||||
sPCAppearanceData PCAppearanceData;
|
||||
};
|
||||
|
||||
struct sP_FE2CL_PC_MOVE {
|
||||
uint64_t iCliTime;
|
||||
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
float fVX;
|
||||
float fVY;
|
||||
float fVZ;
|
||||
int32_t iAngle;
|
||||
uint8_t cKeyValue;
|
||||
|
||||
int32_t iSpeed;
|
||||
|
||||
int32_t iID;
|
||||
|
||||
uint64_t iSvrTime;
|
||||
};
|
||||
|
||||
struct sP_FE2CL_PC_STOP {
|
||||
uint64_t iCliTime;
|
||||
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
int32_t iID;
|
||||
|
||||
uint64_t iSvrTime;
|
||||
};
|
||||
|
||||
struct sP_FE2CL_PC_JUMP {
|
||||
uint64_t iCliTime;
|
||||
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
int32_t iVX;
|
||||
int32_t iVY;
|
||||
int32_t iVZ;
|
||||
int32_t iAngle;
|
||||
uint8_t cKeyValue;
|
||||
int32_t iSpeed;
|
||||
int32_t iID;
|
||||
|
||||
uint64_t iSvrTime;
|
||||
};
|
||||
|
||||
struct sP_FE2CL_PC_MOVEPLATFORM {
|
||||
uint64_t iCliTime;
|
||||
|
||||
int32_t iLcX;
|
||||
int32_t iLcY;
|
||||
int32_t iLcZ;
|
||||
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
|
||||
float fVX;
|
||||
float fVY;
|
||||
float fVZ;
|
||||
|
||||
int32_t bDown;
|
||||
uint32_t iPlatformID;
|
||||
int32_t iAngle;
|
||||
int8_t cKeyValue;
|
||||
int32_t iSpeed;
|
||||
int32_t iPC_ID;
|
||||
uint64_t iSvrTime;
|
||||
};
|
||||
|
||||
struct sP_FE2CL_GM_REP_PC_SET_VALUE {
|
||||
int32_t iPC_ID;
|
||||
int32_t iSetValueType;
|
||||
int32_t iSetValue;
|
||||
};
|
||||
|
||||
struct sP_FE2CL_PC_EXIT {
|
||||
int32_t iID;
|
||||
int32_t iExitType;
|
||||
};
|
||||
|
||||
struct sP_FE2CL_REP_PC_GOTO_SUCC {
|
||||
int32_t iX;
|
||||
int32_t iY;
|
||||
int32_t iZ;
|
||||
};
|
||||
|
||||
struct sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT {
|
||||
int32_t iID_From;
|
||||
int32_t iEmoteCode;
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
|
||||
#endif
|
||||
319
src/Chat.cpp
Normal file
319
src/Chat.cpp
Normal file
@@ -0,0 +1,319 @@
|
||||
#include "Chat.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Groups.hpp"
|
||||
#include "CustomCommands.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
std::vector<std::string> Chat::dump;
|
||||
|
||||
using namespace Chat;
|
||||
|
||||
static void chatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto chat = (sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
|
||||
if (fullChat.length() > 1 && fullChat[0] == CMD_PREFIX) { // PREFIX
|
||||
CustomCommands::runCmd(fullChat, sock);
|
||||
return;
|
||||
}
|
||||
|
||||
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT)
|
||||
return;
|
||||
|
||||
std::string logLine = "[FreeChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
|
||||
// send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC, resp);
|
||||
|
||||
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
|
||||
resp.iPC_ID = plr->iID;
|
||||
resp.iEmoteCode = chat->iEmoteCode;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC);
|
||||
|
||||
// send to visible players
|
||||
PlayerManager::sendToViewable(sock, resp, P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC);
|
||||
}
|
||||
|
||||
static void menuChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto chat = (sP_CL2FE_REQ_SEND_MENUCHAT_MESSAGE*)data->buf;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
|
||||
std::string logLine = "[MenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
|
||||
// send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, resp);
|
||||
|
||||
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
|
||||
resp.iPC_ID = PlayerManager::getPlayer(sock)->iID;
|
||||
resp.iEmoteCode = chat->iEmoteCode;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC);
|
||||
|
||||
// send to visible players
|
||||
PlayerManager::sendToViewable(sock, resp, P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC);
|
||||
}
|
||||
|
||||
static void emoteHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto emote = (sP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, resp);
|
||||
resp.iEmoteCode = emote->iEmoteCode;
|
||||
resp.iID_From = plr->iID;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT);
|
||||
|
||||
// send to visible players (players within render distance)
|
||||
PlayerManager::sendToViewable(sock, resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT);
|
||||
}
|
||||
|
||||
void Chat::sendServerMessage(CNSocket* sock, std::string msg) {
|
||||
INITSTRUCT(sP_FE2CL_PC_MOTD_LOGIN, motd);
|
||||
|
||||
motd.iType = 1;
|
||||
// convert string to u16 and write it to the buffer
|
||||
U8toU16(msg, (char16_t*)motd.szSystemMsg, sizeof(motd.szSystemMsg));
|
||||
|
||||
// send the packet :)
|
||||
sock->sendPacket(motd, P_FE2CL_PC_MOTD_LOGIN);
|
||||
}
|
||||
|
||||
static void announcementHandler(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
if (plr->accountLevel > 30)
|
||||
return; // only players with account level less than 30 (GM) are allowed to use this command
|
||||
auto announcement = (sP_CL2FE_GM_REQ_PC_ANNOUNCE*)data->buf;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
|
||||
msg.iAnnounceType = announcement->iAnnounceType;
|
||||
msg.iDuringTime = announcement->iDuringTime;
|
||||
memcpy(msg.szAnnounceMsg, announcement->szAnnounceMsg, sizeof(msg.szAnnounceMsg));
|
||||
std::map<CNSocket*, Player*>::iterator it;
|
||||
|
||||
switch (announcement->iAreaType) {
|
||||
case 0: // area (all players in viewable chunks)
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
PlayerManager::sendToViewable(sock, msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
break;
|
||||
case 1: // shard
|
||||
case 2: // world
|
||||
break; // not applicable to OpenFusion
|
||||
case 3: // global (all players)
|
||||
for (it = PlayerManager::players.begin(); it != PlayerManager::players.end(); it++) {
|
||||
CNSocket* allSock = it->first;
|
||||
allSock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
std::string logLine = "[Bcast " + std::to_string(announcement->iAreaType) + "] " + PlayerManager::getPlayerName(plr, false) + ": " + AUTOU16TOU8(msg.szAnnounceMsg);
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back("**" + logLine + "**");
|
||||
}
|
||||
|
||||
// Buddy freechatting
|
||||
static void buddyChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC, resp);
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return; // buddy offline
|
||||
|
||||
Player *otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
resp.iFromPCUID = plr->PCStyle.iPC_UID;
|
||||
resp.iToPCUID = pkt->iBuddyPCUID;
|
||||
resp.iEmoteCode = pkt->iEmoteCode;
|
||||
|
||||
std::string fullChat = sanitizeText(AUTOU16TOU8(pkt->szFreeChat));
|
||||
|
||||
if (fullChat.length() > 1 && fullChat[0] == CMD_PREFIX) { // PREFIX
|
||||
CustomCommands::runCmd(fullChat, sock);
|
||||
return;
|
||||
}
|
||||
|
||||
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT)
|
||||
return;
|
||||
|
||||
std::string logLine = "[BuddyChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
|
||||
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC); // confirm send to sender
|
||||
otherSock->sendPacket(resp, P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC); // broadcast send to receiver
|
||||
}
|
||||
|
||||
// Buddy menuchat
|
||||
static void buddyMenuChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC, resp);
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return; // buddy offline
|
||||
|
||||
Player *otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
resp.iFromPCUID = plr->PCStyle.iPC_UID;
|
||||
resp.iToPCUID = pkt->iBuddyPCUID;
|
||||
resp.iEmoteCode = pkt->iEmoteCode;
|
||||
|
||||
std::string fullChat = sanitizeText(AUTOU16TOU8(pkt->szFreeChat));
|
||||
std::string logLine = "[BuddyMenuChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
|
||||
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC); // confirm send to sender
|
||||
otherSock->sendPacket(resp, P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC); // broadcast send to receiver
|
||||
}
|
||||
|
||||
static void tradeChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto pacdat = (sP_CL2FE_REQ_PC_TRADE_EMOTES_CHAT*)data->buf;
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player *otherPlr = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_EMOTES_CHAT, resp);
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
std::string fullChat = sanitizeText(AUTOU16TOU8(pacdat->szFreeChat));
|
||||
U8toU16(fullChat, resp.szFreeChat, sizeof(resp.szFreeChat));
|
||||
|
||||
std::string logLine = "[TradeChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
|
||||
resp.iEmoteCode = pacdat->iEmoteCode;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_TRADE_EMOTES_CHAT);
|
||||
otherSock->sendPacket(resp, P_FE2CL_REP_PC_TRADE_EMOTES_CHAT);
|
||||
}
|
||||
|
||||
static void groupChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
|
||||
|
||||
if (fullChat.length() > 1 && fullChat[0] == CMD_PREFIX) { // PREFIX
|
||||
CustomCommands::runCmd(fullChat, sock);
|
||||
return;
|
||||
}
|
||||
|
||||
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT)
|
||||
return;
|
||||
|
||||
std::string logLine = "[GroupChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
|
||||
// send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, resp);
|
||||
|
||||
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
|
||||
resp.iSendPCID = plr->iID;
|
||||
resp.iEmoteCode = chat->iEmoteCode;
|
||||
|
||||
Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
|
||||
}
|
||||
|
||||
static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
|
||||
std::string logLine = "[GroupMenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
|
||||
|
||||
std::cout << logLine << std::endl;
|
||||
dump.push_back(logLine);
|
||||
|
||||
// send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, resp);
|
||||
|
||||
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
|
||||
resp.iSendPCID = plr->iID;
|
||||
resp.iEmoteCode = chat->iEmoteCode;
|
||||
|
||||
Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
|
||||
}
|
||||
|
||||
// we only allow plain ascii, at least for now
|
||||
std::string Chat::sanitizeText(std::string text, bool allowNewlines) {
|
||||
int i;
|
||||
const int BUFSIZE = 512;
|
||||
char buf[BUFSIZE];
|
||||
|
||||
assert(text.size() < BUFSIZE);
|
||||
|
||||
i = 0;
|
||||
for (char c : text) {
|
||||
if (i >= BUFSIZE-1)
|
||||
break;
|
||||
|
||||
if (!allowNewlines && c == '\n')
|
||||
continue;
|
||||
|
||||
if ((c >= ' ' && c <= '~') || c == '\n')
|
||||
buf[i++] = c;
|
||||
}
|
||||
buf[i] = 0;
|
||||
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
void Chat::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_FREECHAT_MESSAGE, chatHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT, emoteHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE, menuChatHandler);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE, buddyChatHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE, buddyMenuChatHandler);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_EMOTES_CHAT, tradeChatHandler);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE, groupChatHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE, groupMenuChatHandler);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_ANNOUNCE, announcementHandler);
|
||||
}
|
||||
13
src/Chat.hpp
Normal file
13
src/Chat.hpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#define CMD_PREFIX '/'
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
namespace Chat {
|
||||
extern std::vector<std::string> dump;
|
||||
void init();
|
||||
|
||||
void sendServerMessage(CNSocket* sock, std::string msg); // uses MOTD
|
||||
std::string sanitizeText(std::string text, bool allowNewlines=false);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#include "CNShardServer.hpp"
|
||||
#include "CNStructs.hpp"
|
||||
#include "ChatManager.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
|
||||
void ChatManager::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_FREECHAT_MESSAGE, chatHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT, emoteHandler);
|
||||
}
|
||||
|
||||
void ChatManager::chatHandler(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE*)data->buf;
|
||||
|
||||
// stubbed for now
|
||||
}
|
||||
|
||||
void ChatManager::emoteHandler(CNSocket* sock, CNPacketData* data) {
|
||||
// you can dance with friends!!!!!!!!
|
||||
|
||||
sP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT* emote = (sP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT*)data->buf;
|
||||
|
||||
PlayerView plr = PlayerManager::players[sock];
|
||||
|
||||
// send to client
|
||||
sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT* resp = (sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT*)xmalloc(sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT));
|
||||
resp->iEmoteCode = emote->iEmoteCode;
|
||||
resp->iID_From = plr.plr.iID;
|
||||
sock->sendPacket(new CNPacketData((void*)resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT), sock->getFEKey()));
|
||||
|
||||
// send to visible players (players within render distance)
|
||||
for (CNSocket* otherSock : plr.viewable) {
|
||||
resp = (sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT*)xmalloc(sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT));
|
||||
resp->iEmoteCode = emote->iEmoteCode;
|
||||
resp->iID_From = plr.plr.iID;
|
||||
otherSock->sendPacket(new CNPacketData((void*)resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT), otherSock->getFEKey()));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#ifndef _CM_HPP
|
||||
#define _CM_HPP
|
||||
|
||||
#include "CNShardServer.hpp"
|
||||
|
||||
namespace ChatManager {
|
||||
void init();
|
||||
|
||||
void chatHandler(CNSocket* sock, CNPacketData* data);
|
||||
void emoteHandler(CNSocket* sock, CNPacketData* data);
|
||||
}
|
||||
|
||||
#endif
|
||||
345
src/Chunking.cpp
Normal file
345
src/Chunking.cpp
Normal file
@@ -0,0 +1,345 @@
|
||||
#include "Chunking.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "Combat.hpp"
|
||||
#include "Eggs.hpp"
|
||||
|
||||
using namespace Chunking;
|
||||
|
||||
/*
|
||||
* The initial chunkPos value before a player is placed into the world.
|
||||
*/
|
||||
const ChunkPos Chunking::INVALID_CHUNK = {};
|
||||
|
||||
std::map<ChunkPos, Chunk*> Chunking::chunks;
|
||||
|
||||
static void newChunk(ChunkPos pos) {
|
||||
if (chunkExists(pos)) {
|
||||
std::cout << "[WARN] Tried to create a chunk that already exists" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
Chunk *chunk = new Chunk();
|
||||
chunks[pos] = chunk;
|
||||
|
||||
// add the chunk to the cache of all players and NPCs in the surrounding chunks
|
||||
std::set<Chunk*> surroundings = getViewableChunks(pos);
|
||||
for (Chunk* c : surroundings)
|
||||
for (const EntityRef& ref : c->entities)
|
||||
ref.getEntity()->viewableChunks.insert(chunk);
|
||||
}
|
||||
|
||||
static void deleteChunk(ChunkPos pos) {
|
||||
if (!chunkExists(pos)) {
|
||||
std::cout << "[WARN] Tried to delete a chunk that doesn't exist" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
Chunk* chunk = chunks[pos];
|
||||
|
||||
// remove the chunk from the cache of all players and NPCs in the surrounding chunks
|
||||
std::set<Chunk*> surroundings = getViewableChunks(pos);
|
||||
for(Chunk* c : surroundings)
|
||||
for (const EntityRef& ref : c->entities)
|
||||
ref.getEntity()->viewableChunks.erase(chunk);
|
||||
|
||||
chunks.erase(pos); // remove from map
|
||||
delete chunk; // free from memory
|
||||
}
|
||||
|
||||
void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef& ref) {
|
||||
if (!chunkExists(chunkPos))
|
||||
return; // shouldn't happen
|
||||
|
||||
chunks[chunkPos]->entities.insert(ref);
|
||||
|
||||
if (ref.type == EntityType::PLAYER)
|
||||
chunks[chunkPos]->nplayers++;
|
||||
}
|
||||
|
||||
void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef& ref) {
|
||||
if (!chunkExists(chunkPos))
|
||||
return; // do nothing if chunk doesn't even exist
|
||||
|
||||
Chunk* chunk = chunks[chunkPos];
|
||||
|
||||
chunk->entities.erase(ref); // gone
|
||||
|
||||
if (ref.type == EntityType::PLAYER)
|
||||
chunks[chunkPos]->nplayers--;
|
||||
assert(chunks[chunkPos]->nplayers >= 0);
|
||||
|
||||
// if chunk is completely empty, free it
|
||||
if (chunk->entities.size() == 0)
|
||||
deleteChunk(chunkPos);
|
||||
}
|
||||
|
||||
void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
|
||||
Entity *ent = ref.getEntity();
|
||||
bool alive = ent->isAlive();
|
||||
|
||||
// TODO: maybe optimize this, potentially using AROUND packets?
|
||||
for (Chunk *chunk : chnks) {
|
||||
for (const EntityRef& otherRef : chunk->entities) {
|
||||
// skip oneself
|
||||
if (ref == otherRef)
|
||||
continue;
|
||||
|
||||
Entity *other = otherRef.getEntity();
|
||||
|
||||
// notify all visible players of the existence of this Entity
|
||||
if (alive && otherRef.type == EntityType::PLAYER) {
|
||||
ent->enterIntoViewOf(otherRef.sock);
|
||||
}
|
||||
|
||||
// notify this *player* of the existence of all visible Entities
|
||||
if (ref.type == EntityType::PLAYER && other->isAlive()) {
|
||||
other->enterIntoViewOf(ref.sock);
|
||||
}
|
||||
|
||||
// for mobs, increment playersInView
|
||||
if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER)
|
||||
((Mob*)ent)->playersInView++;
|
||||
if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER)
|
||||
((Mob*)other)->playersInView++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
|
||||
Entity *ent = ref.getEntity();
|
||||
bool alive = ent->isAlive();
|
||||
|
||||
// TODO: same as above
|
||||
for (Chunk *chunk : chnks) {
|
||||
for (const EntityRef& otherRef : chunk->entities) {
|
||||
// skip oneself
|
||||
if (ref == otherRef)
|
||||
continue;
|
||||
|
||||
Entity *other = otherRef.getEntity();
|
||||
|
||||
// notify all visible players of the departure of this Entity
|
||||
if (alive && otherRef.type == EntityType::PLAYER) {
|
||||
ent->disappearFromViewOf(otherRef.sock);
|
||||
}
|
||||
|
||||
// notify this *player* of the departure of all visible Entities
|
||||
if (ref.type == EntityType::PLAYER && other->isAlive()) {
|
||||
other->disappearFromViewOf(ref.sock);
|
||||
}
|
||||
|
||||
// for mobs, decrement playersInView
|
||||
if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER)
|
||||
((Mob*)ent)->playersInView--;
|
||||
if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER)
|
||||
((Mob*)other)->playersInView--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void emptyChunk(ChunkPos chunkPos) {
|
||||
if (!chunkExists(chunkPos)) {
|
||||
std::cout << "[WARN] Tried to empty chunk that doesn't exist\n";
|
||||
return; // chunk doesn't exist, we don't need to do anything
|
||||
}
|
||||
|
||||
Chunk* chunk = chunks[chunkPos];
|
||||
|
||||
if (chunk->nplayers > 0) {
|
||||
std::cout << "[WARN] Tried to empty chunk that still had players\n";
|
||||
return; // chunk doesn't exist, we don't need to do anything
|
||||
}
|
||||
|
||||
// unspawn all of the mobs/npcs
|
||||
std::set refs(chunk->entities);
|
||||
for (const EntityRef& ref : refs) {
|
||||
if (ref.type == EntityType::PLAYER)
|
||||
assert(0);
|
||||
|
||||
// every call of this will check if the chunk is empty and delete it if so
|
||||
NPCManager::destroyNPC(ref.id);
|
||||
}
|
||||
}
|
||||
|
||||
void Chunking::updateEntityChunk(const EntityRef& ref, ChunkPos from, ChunkPos to) {
|
||||
Entity* ent = ref.getEntity();
|
||||
|
||||
// move to other chunk's player set
|
||||
untrackEntity(from, ref); // this will delete the chunk if it's empty
|
||||
|
||||
// if the new chunk doesn't exist, make it first
|
||||
if (!chunkExists(to))
|
||||
newChunk(to);
|
||||
|
||||
trackEntity(to, ref);
|
||||
|
||||
// calculate viewable chunks from both points
|
||||
std::set<Chunk*> oldViewables = getViewableChunks(from);
|
||||
std::set<Chunk*> newViewables = getViewableChunks(to);
|
||||
std::set<Chunk*> toExit, toEnter;
|
||||
|
||||
/*
|
||||
* Calculate diffs. This is done to prevent phasing on chunk borders.
|
||||
* toExit will contain old viewables - new viewables, so the player will only be exited in chunks that are out of sight.
|
||||
* toEnter contains the opposite: new viewables - old viewables, chunks where we previously weren't visible from before.
|
||||
*/
|
||||
std::set_difference(oldViewables.begin(), oldViewables.end(), newViewables.begin(), newViewables.end(),
|
||||
std::inserter(toExit, toExit.end())); // chunks we must be EXITed from (old - new)
|
||||
std::set_difference(newViewables.begin(), newViewables.end(), oldViewables.begin(), oldViewables.end(),
|
||||
std::inserter(toEnter, toEnter.end())); // chunks we must be ENTERed into (new - old)
|
||||
|
||||
// update views
|
||||
removeEntityFromChunks(toExit, ref);
|
||||
addEntityToChunks(toEnter, ref);
|
||||
|
||||
ent->chunkPos = to; // update cached chunk position
|
||||
// updated cached viewable chunks
|
||||
ent->viewableChunks.clear();
|
||||
ent->viewableChunks.insert(newViewables.begin(), newViewables.end());
|
||||
}
|
||||
|
||||
bool Chunking::chunkExists(ChunkPos chunk) {
|
||||
return chunks.find(chunk) != chunks.end();
|
||||
}
|
||||
|
||||
ChunkPos Chunking::chunkPosAt(int posX, int posY, uint64_t instanceID) {
|
||||
return ChunkPos(posX / (settings::VIEWDISTANCE / 3), posY / (settings::VIEWDISTANCE / 3), instanceID);
|
||||
}
|
||||
|
||||
std::set<Chunk*> Chunking::getViewableChunks(ChunkPos chunk) {
|
||||
std::set<Chunk*> chnks;
|
||||
|
||||
int x, y;
|
||||
uint64_t inst;
|
||||
std::tie(x, y, inst) = chunk;
|
||||
|
||||
// grabs surrounding chunks if they exist
|
||||
for (int i = -1; i < 2; i++) {
|
||||
for (int z = -1; z < 2; z++) {
|
||||
ChunkPos pos = ChunkPos(x+i, y+z, inst);
|
||||
|
||||
// if chunk exists, add it to the set
|
||||
if (chunkExists(pos))
|
||||
chnks.insert(chunks[pos]);
|
||||
}
|
||||
}
|
||||
|
||||
return chnks;
|
||||
}
|
||||
|
||||
/*
|
||||
* inefficient algorithm to get all chunks from a specific instance
|
||||
*/
|
||||
std::vector<ChunkPos> Chunking::getChunksInMap(uint64_t mapNum) {
|
||||
std::vector<ChunkPos> chnks;
|
||||
|
||||
for (auto it = chunks.begin(); it != chunks.end(); it++) {
|
||||
if (std::get<2>(it->first) == mapNum) {
|
||||
chnks.push_back(it->first);
|
||||
}
|
||||
}
|
||||
|
||||
return chnks;
|
||||
}
|
||||
|
||||
/*
|
||||
* Used only for eggs; use npc->playersInView for everything visible
|
||||
*/
|
||||
bool Chunking::inPopulatedChunks(std::set<Chunk*>* chnks) {
|
||||
for (auto it = chnks->begin(); it != chnks->end(); it++) {
|
||||
if ((*it)->nplayers > 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Chunking::createInstance(uint64_t instanceID) {
|
||||
std::vector<ChunkPos> templateChunks = getChunksInMap(MAPNUM(instanceID)); // base instance chunks
|
||||
|
||||
// only instantiate if the instance doesn't exist already
|
||||
if (getChunksInMap(instanceID).size() != 0) {
|
||||
std::cout << "Instance " << instanceID << " already exists" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "Creating instance " << instanceID << std::endl;
|
||||
for (ChunkPos &coords : templateChunks) {
|
||||
for (const EntityRef& ref : chunks[coords]->entities) {
|
||||
if (ref.type == EntityType::PLAYER)
|
||||
continue;
|
||||
|
||||
int npcID = ref.id;
|
||||
BaseNPC* baseNPC = (BaseNPC*)ref.getEntity();
|
||||
|
||||
// make a copy of each NPC in the template chunks and put them in the new instance
|
||||
if (baseNPC->type == EntityType::MOB) {
|
||||
if (((Mob*)baseNPC)->groupLeader != 0 && ((Mob*)baseNPC)->groupLeader != npcID)
|
||||
continue; // follower; don't copy individually
|
||||
|
||||
Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle,
|
||||
instanceID, baseNPC->appearanceData.iNPCType, NPCManager::NPCData[baseNPC->appearanceData.iNPCType], NPCManager::nextId--);
|
||||
NPCManager::NPCs[newMob->appearanceData.iNPC_ID] = newMob;
|
||||
|
||||
// if in a group, copy over group members as well
|
||||
if (((Mob*)baseNPC)->groupLeader != 0) {
|
||||
newMob->groupLeader = newMob->appearanceData.iNPC_ID; // set leader ID for new leader
|
||||
Mob* mobData = (Mob*)baseNPC;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (mobData->groupMember[i] != 0) {
|
||||
int followerID = NPCManager::nextId--; // id for follower
|
||||
BaseNPC* baseFollower = NPCManager::NPCs[mobData->groupMember[i]]; // follower from template
|
||||
// new follower instance
|
||||
Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->appearanceData.iAngle,
|
||||
instanceID, baseFollower->appearanceData.iNPCType, NPCManager::NPCData[baseFollower->appearanceData.iNPCType], followerID);
|
||||
// add follower to NPC maps
|
||||
NPCManager::NPCs[followerID] = newMobFollower;
|
||||
// set follower-specific properties
|
||||
newMobFollower->groupLeader = newMob->appearanceData.iNPC_ID;
|
||||
newMobFollower->offsetX = ((Mob*)baseFollower)->offsetX;
|
||||
newMobFollower->offsetY = ((Mob*)baseFollower)->offsetY;
|
||||
// add follower copy to leader copy
|
||||
newMob->groupMember[i] = followerID;
|
||||
NPCManager::updateNPCPosition(followerID, baseFollower->x, baseFollower->y, baseFollower->z,
|
||||
instanceID, baseFollower->appearanceData.iAngle);
|
||||
}
|
||||
}
|
||||
}
|
||||
NPCManager::updateNPCPosition(newMob->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z,
|
||||
instanceID, baseNPC->appearanceData.iAngle);
|
||||
} else {
|
||||
BaseNPC* newNPC = new BaseNPC(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle,
|
||||
instanceID, baseNPC->appearanceData.iNPCType, NPCManager::nextId--);
|
||||
NPCManager::NPCs[newNPC->appearanceData.iNPC_ID] = newNPC;
|
||||
NPCManager::updateNPCPosition(newNPC->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z,
|
||||
instanceID, baseNPC->appearanceData.iAngle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void destroyInstance(uint64_t instanceID) {
|
||||
std::vector<ChunkPos> instanceChunks = getChunksInMap(instanceID);
|
||||
std::cout << "Deleting instance " << instanceID << " (" << instanceChunks.size() << " chunks)" << std::endl;
|
||||
for (ChunkPos& coords : instanceChunks) {
|
||||
emptyChunk(coords);
|
||||
}
|
||||
}
|
||||
|
||||
void Chunking::destroyInstanceIfEmpty(uint64_t instanceID) {
|
||||
if (PLAYERID(instanceID) == 0)
|
||||
return; // don't clean up overworld/IZ chunks
|
||||
|
||||
std::vector<ChunkPos> sourceChunkCoords = getChunksInMap(instanceID);
|
||||
|
||||
for (ChunkPos& coords : sourceChunkCoords) {
|
||||
Chunk* chunk = chunks[coords];
|
||||
|
||||
if (chunk->nplayers > 0)
|
||||
return; // there are still players inside
|
||||
}
|
||||
|
||||
destroyInstance(instanceID);
|
||||
}
|
||||
55
src/Chunking.hpp
Normal file
55
src/Chunking.hpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
#include <utility>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
#include <algorithm>
|
||||
|
||||
struct EntityRef;
|
||||
|
||||
class Chunk {
|
||||
public:
|
||||
std::set<EntityRef> entities;
|
||||
int nplayers = 0;
|
||||
};
|
||||
|
||||
// to help the readability of ChunkPos
|
||||
typedef std::tuple<int, int, uint64_t> _ChunkPos;
|
||||
|
||||
class ChunkPos : public _ChunkPos {
|
||||
public:
|
||||
ChunkPos() : _ChunkPos(0, 0, (uint64_t) -1) {}
|
||||
ChunkPos(int x, int y, uint64_t inst) : _ChunkPos(x, y, inst) {}
|
||||
};
|
||||
|
||||
enum {
|
||||
INSTANCE_OVERWORLD, // default instance every player starts in
|
||||
INSTANCE_IZ, // these aren't actually used
|
||||
INSTANCE_UNIQUE // these aren't actually used
|
||||
};
|
||||
|
||||
namespace Chunking {
|
||||
extern std::map<ChunkPos, Chunk*> chunks;
|
||||
|
||||
extern const ChunkPos INVALID_CHUNK;
|
||||
|
||||
void updateEntityChunk(const EntityRef& ref, ChunkPos from, ChunkPos to);
|
||||
|
||||
void trackEntity(ChunkPos chunkPos, const EntityRef& ref);
|
||||
void untrackEntity(ChunkPos chunkPos, const EntityRef& ref);
|
||||
|
||||
void addEntityToChunks(std::set<Chunk*> chnks, const EntityRef& ref);
|
||||
void removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef& ref);
|
||||
|
||||
bool chunkExists(ChunkPos chunk);
|
||||
ChunkPos chunkPosAt(int posX, int posY, uint64_t instanceID);
|
||||
std::set<Chunk*> getViewableChunks(ChunkPos chunkPos);
|
||||
std::vector<ChunkPos> getChunksInMap(uint64_t mapNum);
|
||||
|
||||
bool inPopulatedChunks(std::set<Chunk*>* chnks);
|
||||
void createInstance(uint64_t);
|
||||
void destroyInstanceIfEmpty(uint64_t);
|
||||
}
|
||||
824
src/Combat.cpp
Normal file
824
src/Combat.cpp
Normal file
@@ -0,0 +1,824 @@
|
||||
#include "Combat.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Groups.hpp"
|
||||
#include "Transport.hpp"
|
||||
#include "Racing.hpp"
|
||||
#include "Abilities.hpp"
|
||||
#include "Rand.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
using namespace Combat;
|
||||
|
||||
/// Player Id -> Bullet Id -> Bullet
|
||||
std::map<int32_t, std::map<int8_t, Bullet>> Combat::Bullets;
|
||||
|
||||
static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shouldCrit,
|
||||
bool batteryBoost, int attackerStyle,
|
||||
int defenderStyle, int difficulty) {
|
||||
std::pair<int,int> ret = {0, 1};
|
||||
if (attackPower + defensePower * 2 == 0)
|
||||
return ret;
|
||||
|
||||
// base calculation
|
||||
int damage = attackPower * attackPower / (attackPower + defensePower);
|
||||
damage = std::max(10 + attackPower / 10, damage - (defensePower - attackPower / 6) * difficulty / 100);
|
||||
damage = damage * (Rand::rand(40) + 80) / 100;
|
||||
|
||||
// Adaptium/Blastons/Cosmix
|
||||
if (attackerStyle != -1 && defenderStyle != -1 && attackerStyle != defenderStyle) {
|
||||
if (attackerStyle - defenderStyle == 2)
|
||||
defenderStyle += 3;
|
||||
if (defenderStyle - attackerStyle == 2)
|
||||
defenderStyle -= 3;
|
||||
if (attackerStyle < defenderStyle)
|
||||
damage = damage * 5 / 4;
|
||||
else
|
||||
damage = damage * 4 / 5;
|
||||
}
|
||||
|
||||
// weapon boosts
|
||||
if (batteryBoost)
|
||||
damage = damage * 5 / 4;
|
||||
|
||||
ret.first = damage;
|
||||
ret.second = 1;
|
||||
|
||||
if (shouldCrit && Rand::rand(20) == 0) {
|
||||
ret.first *= 2; // critical hit
|
||||
ret.second = 2;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool checkRapidFire(CNSocket *sock, int targetCount) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
time_t currTime = getTime();
|
||||
|
||||
if (currTime - plr->lastShot < plr->fireRate * 80)
|
||||
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
|
||||
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
|
||||
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing
|
||||
|
||||
plr->lastShot = currTime;
|
||||
|
||||
// 3+ targets should never be possible
|
||||
if (targetCount > 3)
|
||||
plr->suspicionRating += 10001;
|
||||
|
||||
// kill the socket when the player is too suspicious
|
||||
if (plr->suspicionRating > 10000) {
|
||||
sock->kill();
|
||||
CNShardServer::_killConnection(sock);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
|
||||
auto pkt = (sP_CL2FE_REQ_PC_ATTACK_NPCs*)data->buf;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
auto targets = (int32_t*)data->trailers;
|
||||
|
||||
// kick the player if firing too rapidly
|
||||
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt))
|
||||
return;
|
||||
|
||||
/*
|
||||
* IMPORTANT: This validates memory safety in addition to preventing
|
||||
* ordinary cheating. If the client sends a very large number of trailing
|
||||
* values, it could overflow the *response* buffer, which isn't otherwise
|
||||
* being validated anymore.
|
||||
*/
|
||||
if (pkt->iNPCCnt > 3) {
|
||||
std::cout << "[WARN] Player tried to attack more than 3 NPCs at once" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
INITVARPACKET(respbuf, sP_FE2CL_PC_ATTACK_NPCs_SUCC, resp, sAttackResult, respdata);
|
||||
|
||||
resp->iNPCCnt = pkt->iNPCCnt;
|
||||
|
||||
for (int i = 0; i < data->trCnt; i++) {
|
||||
if (NPCManager::NPCs.find(targets[i]) == NPCManager::NPCs.end()) {
|
||||
// not sure how to best handle this
|
||||
std::cout << "[WARN] pcAttackNpcs: NPC ID not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
BaseNPC* npc = NPCManager::NPCs[targets[i]];
|
||||
if (npc->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] pcAttackNpcs: NPC is not a mob" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
Mob* mob = (Mob*)npc;
|
||||
|
||||
std::pair<int,int> damage;
|
||||
|
||||
if (pkt->iNPCCnt > 1)
|
||||
damage.first = plr->groupDamage;
|
||||
else
|
||||
damage.first = plr->pointDamage;
|
||||
|
||||
int difficulty = (int)mob->data["m_iNpcLevel"];
|
||||
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
|
||||
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
|
||||
|
||||
if (plr->batteryW >= 6 + difficulty)
|
||||
plr->batteryW -= 6 + difficulty;
|
||||
else
|
||||
plr->batteryW = 0;
|
||||
|
||||
damage.first = hitMob(sock, mob, damage.first);
|
||||
|
||||
respdata[i].iID = mob->appearanceData.iNPC_ID;
|
||||
respdata[i].iDamage = damage.first;
|
||||
respdata[i].iHP = mob->appearanceData.iHP;
|
||||
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
|
||||
}
|
||||
|
||||
resp->iBatteryW = plr->batteryW;
|
||||
sock->sendPacket(respbuf, P_FE2CL_PC_ATTACK_NPCs_SUCC);
|
||||
|
||||
// a bit of a hack: these are the same size, so we can reuse the response packet
|
||||
assert(sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC) == sizeof(sP_FE2CL_PC_ATTACK_NPCs));
|
||||
auto *resp1 = (sP_FE2CL_PC_ATTACK_NPCs*)respbuf;
|
||||
|
||||
resp1->iPC_ID = plr->iID;
|
||||
|
||||
// send to other players
|
||||
PlayerManager::sendToViewable(sock, respbuf, P_FE2CL_PC_ATTACK_NPCs);
|
||||
}
|
||||
|
||||
void Combat::npcAttackPc(Mob *mob, time_t currTime) {
|
||||
Player *plr = PlayerManager::getPlayer(mob->target);
|
||||
|
||||
INITVARPACKET(respbuf, sP_FE2CL_NPC_ATTACK_PCs, pkt, sAttackResult, atk);
|
||||
|
||||
auto damage = getDamage(450 + (int)mob->data["m_iPower"], plr->defense, true, false, -1, -1, 0);
|
||||
|
||||
if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
|
||||
plr->HP -= damage.first;
|
||||
|
||||
pkt->iNPC_ID = mob->appearanceData.iNPC_ID;
|
||||
pkt->iPCCnt = 1;
|
||||
|
||||
atk->iID = plr->iID;
|
||||
atk->iDamage = damage.first;
|
||||
atk->iHP = plr->HP;
|
||||
atk->iHitFlag = damage.second;
|
||||
|
||||
mob->target->sendPacket(respbuf, P_FE2CL_NPC_ATTACK_PCs);
|
||||
PlayerManager::sendToViewable(mob->target, respbuf, P_FE2CL_NPC_ATTACK_PCs);
|
||||
|
||||
if (plr->HP <= 0) {
|
||||
mob->target = nullptr;
|
||||
mob->state = MobState::RETREAT;
|
||||
if (!MobAI::aggroCheck(mob, currTime)) {
|
||||
MobAI::clearDebuff(mob);
|
||||
if (mob->groupLeader != 0)
|
||||
MobAI::groupRetreat(mob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Combat::hitMob(CNSocket *sock, Mob *mob, int damage) {
|
||||
// cannot kill mobs multiple times; cannot harm retreating mobs
|
||||
if (mob->state != MobState::ROAMING && mob->state != MobState::COMBAT) {
|
||||
return 0; // no damage
|
||||
}
|
||||
|
||||
if (mob->skillStyle >= 0)
|
||||
return 0; // don't hurt a mob casting corruption
|
||||
|
||||
if (mob->state == MobState::ROAMING) {
|
||||
assert(mob->target == nullptr);
|
||||
MobAI::enterCombat(sock, mob);
|
||||
|
||||
if (mob->groupLeader != 0)
|
||||
MobAI::followToCombat(mob);
|
||||
}
|
||||
|
||||
mob->appearanceData.iHP -= damage;
|
||||
|
||||
// wake up sleeping monster
|
||||
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_MEZ) {
|
||||
mob->appearanceData.iConditionBitFlag &= ~CSB_BIT_MEZ;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
|
||||
pkt1.eCT = 2;
|
||||
pkt1.iID = mob->appearanceData.iNPC_ID;
|
||||
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
|
||||
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
||||
}
|
||||
|
||||
if (mob->appearanceData.iHP <= 0)
|
||||
killMob(mob->target, mob);
|
||||
|
||||
return damage;
|
||||
}
|
||||
|
||||
/*
|
||||
* When a group of players is doing missions together, we want them to all get
|
||||
* quest items at the same time, but we don't want the odds of quest item
|
||||
* drops from different missions to be linked together. That's why we use a
|
||||
* single RNG roll per mission task, and every group member shares that same
|
||||
* set of rolls.
|
||||
*/
|
||||
static void genQItemRolls(Player *leader, std::map<int, int>& rolls) {
|
||||
for (int i = 0; i < leader->groupCnt; i++) {
|
||||
if (leader->groupIDs[i] == 0)
|
||||
continue;
|
||||
|
||||
CNSocket *otherSock = PlayerManager::getSockFromID(leader->groupIDs[i]);
|
||||
if (otherSock == nullptr)
|
||||
continue;
|
||||
|
||||
Player *member = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
for (int j = 0; j < ACTIVE_MISSION_COUNT; j++)
|
||||
if (member->tasks[j] != 0)
|
||||
rolls[member->tasks[j]] = Rand::rand();
|
||||
}
|
||||
}
|
||||
|
||||
void Combat::killMob(CNSocket *sock, Mob *mob) {
|
||||
mob->state = MobState::DEAD;
|
||||
mob->target = nullptr;
|
||||
mob->appearanceData.iConditionBitFlag = 0;
|
||||
mob->skillStyle = -1;
|
||||
mob->unbuffTimes.clear();
|
||||
mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
|
||||
|
||||
// check for the edge case where hitting the mob did not aggro it
|
||||
if (sock != nullptr) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
Items::DropRoll rolled;
|
||||
Items::DropRoll eventRolled;
|
||||
std::map<int, int> qitemRolls;
|
||||
|
||||
Player *leader = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
||||
assert(leader != nullptr); // should never happen
|
||||
|
||||
genQItemRolls(leader, qitemRolls);
|
||||
|
||||
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
|
||||
Items::giveMobDrop(sock, mob, rolled, eventRolled);
|
||||
Missions::mobKilled(sock, mob->appearanceData.iNPCType, qitemRolls);
|
||||
} else {
|
||||
for (int i = 0; i < leader->groupCnt; i++) {
|
||||
CNSocket* sockTo = PlayerManager::getSockFromID(leader->groupIDs[i]);
|
||||
if (sockTo == nullptr)
|
||||
continue;
|
||||
|
||||
Player *otherPlr = PlayerManager::getPlayer(sockTo);
|
||||
|
||||
// only contribute to group members' kills if they're close enough
|
||||
int dist = std::hypot(plr->x - otherPlr->x + 1, plr->y - otherPlr->y + 1);
|
||||
if (dist > 5000)
|
||||
continue;
|
||||
|
||||
Items::giveMobDrop(sockTo, mob, rolled, eventRolled);
|
||||
Missions::mobKilled(sockTo, mob->appearanceData.iNPCType, qitemRolls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delay the despawn animation
|
||||
mob->despawned = false;
|
||||
|
||||
// fire any triggered events
|
||||
for (NPCEvent& event : NPCManager::NPCEvents)
|
||||
if (event.trigger == ON_KILLED && event.npcType == mob->appearanceData.iNPCType)
|
||||
event.handler(sock, mob);
|
||||
|
||||
auto it = Transport::NPCQueues.find(mob->appearanceData.iNPC_ID);
|
||||
if (it == Transport::NPCQueues.end() || it->second.empty())
|
||||
return;
|
||||
|
||||
// rewind or empty the movement queue
|
||||
if (mob->staticPath) {
|
||||
/*
|
||||
* This is inelegant, but we wind forward in the path until we find the point that
|
||||
* corresponds with the Mob's spawn point.
|
||||
*
|
||||
* IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever.
|
||||
*/
|
||||
auto& queue = it->second;
|
||||
for (auto point = queue.front(); point.x != mob->spawnX || point.y != mob->spawnY; point = queue.front()) {
|
||||
queue.pop();
|
||||
queue.push(point);
|
||||
}
|
||||
} else {
|
||||
Transport::NPCQueues.erase(mob->appearanceData.iNPC_ID);
|
||||
}
|
||||
}
|
||||
|
||||
static void combatBegin(CNSocket *sock, CNPacketData *data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
plr->inCombat = true;
|
||||
|
||||
// HACK: make sure the player has the right weapon out for combat
|
||||
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, resp);
|
||||
|
||||
resp.iPC_ID = plr->iID;
|
||||
resp.iEquipSlotNum = 0;
|
||||
resp.EquipSlotItem = plr->Equip[0];
|
||||
|
||||
PlayerManager::sendToViewable(sock, (void*)&resp, P_FE2CL_PC_EQUIP_CHANGE, sizeof(sP_FE2CL_PC_EQUIP_CHANGE));
|
||||
}
|
||||
|
||||
static void combatEnd(CNSocket *sock, CNPacketData *data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
plr->inCombat = false;
|
||||
plr->healCooldown = 4000;
|
||||
}
|
||||
|
||||
static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) {
|
||||
sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION) != (bool)pkt->iFlag)
|
||||
plr->iConditionBitFlag ^= CSB_BIT_INFECTION;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt1);
|
||||
|
||||
pkt1.eCSTB = ECSB_INFECTION; // eCharStatusTimeBuffID
|
||||
pkt1.eTBU = 1; // eTimeBuffUpdate
|
||||
pkt1.eTBT = 0; // eTimeBuffType 1 means nano
|
||||
pkt1.iConditionBitFlag = plr->iConditionBitFlag;
|
||||
|
||||
sock->sendPacket((void*)&pkt1, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
|
||||
}
|
||||
|
||||
static void dealGooDamage(CNSocket *sock, int amount) {
|
||||
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_DotDamage);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
|
||||
sSkillResult_DotDamage *dmg = (sSkillResult_DotDamage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
||||
|
||||
if (plr->iConditionBitFlag & CSB_BIT_PROTECT_INFECTION) {
|
||||
amount = -2; // -2 is the magic number for "Protected" to appear as the damage number
|
||||
dmg->bProtected = 1;
|
||||
|
||||
// eggs allow protection without nanos
|
||||
if (plr->activeNano != -1 && (plr->iSelfConditionBitFlag & CSB_BIT_PROTECT_INFECTION))
|
||||
plr->Nanos[plr->activeNano].iStamina -= 3;
|
||||
} else {
|
||||
plr->HP -= amount;
|
||||
}
|
||||
|
||||
if (plr->activeNano != 0) {
|
||||
dmg->iStamina = plr->Nanos[plr->activeNano].iStamina;
|
||||
|
||||
if (plr->Nanos[plr->activeNano].iStamina <= 0) {
|
||||
dmg->bNanoDeactive = 1;
|
||||
plr->Nanos[plr->activeNano].iStamina = 0;
|
||||
Nanos::summonNano(PlayerManager::getSockFromID(plr->iID), -1, true);
|
||||
}
|
||||
}
|
||||
|
||||
pkt->iID = plr->iID;
|
||||
pkt->eCT = 1; // player
|
||||
pkt->iTB_ID = ECSB_INFECTION; // sSkillResult_DotDamage
|
||||
|
||||
dmg->eCT = 1;
|
||||
dmg->iID = plr->iID;
|
||||
dmg->iDamage = amount;
|
||||
dmg->iHP = plr->HP;
|
||||
dmg->iConditionBitFlag = plr->iConditionBitFlag;
|
||||
|
||||
sock->sendPacket((void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
|
||||
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
|
||||
}
|
||||
|
||||
static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
|
||||
sP_CL2FE_REQ_PC_ATTACK_CHARs* pkt = (sP_CL2FE_REQ_PC_ATTACK_CHARs*)data->buf;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// only GMs can use this variant
|
||||
if (plr->accountLevel > 30)
|
||||
return;
|
||||
|
||||
// Unlike the attack mob packet, attacking players packet has an 8-byte trail (Instead of 4 bytes).
|
||||
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(int32_t) * 2, data->size)) {
|
||||
std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_CHARs packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs));
|
||||
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC), pkt->iTargetCnt, sizeof(sAttackResult))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_CHARs_SUCC packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) + pkt->iTargetCnt * sizeof(sAttackResult);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_PC_ATTACK_CHARs_SUCC *resp = (sP_FE2CL_PC_ATTACK_CHARs_SUCC*)respbuf;
|
||||
sAttackResult *respdata = (sAttackResult*)(respbuf+sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC));
|
||||
|
||||
resp->iTargetCnt = pkt->iTargetCnt;
|
||||
|
||||
for (int i = 0; i < pkt->iTargetCnt; i++) {
|
||||
if (pktdata[i*2+1] == 1) { // eCT == 1; attack player
|
||||
Player *target = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == pktdata[i*2]) {
|
||||
target = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (target == nullptr) {
|
||||
// you shall not pass
|
||||
std::cout << "[WARN] pcAttackChars: player ID not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::pair<int,int> damage;
|
||||
|
||||
if (pkt->iTargetCnt > 1)
|
||||
damage.first = plr->groupDamage;
|
||||
else
|
||||
damage.first = plr->pointDamage;
|
||||
|
||||
damage = getDamage(damage.first, target->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0);
|
||||
|
||||
if (plr->batteryW >= 6 + plr->level)
|
||||
plr->batteryW -= 6 + plr->level;
|
||||
else
|
||||
plr->batteryW = 0;
|
||||
|
||||
target->HP -= damage.first;
|
||||
|
||||
respdata[i].eCT = pktdata[i*2+1];
|
||||
respdata[i].iID = target->iID;
|
||||
respdata[i].iDamage = damage.first;
|
||||
respdata[i].iHP = target->HP;
|
||||
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
|
||||
} else { // eCT == 4; attack mob
|
||||
if (NPCManager::NPCs.find(pktdata[i*2]) == NPCManager::NPCs.end()) {
|
||||
// not sure how to best handle this
|
||||
std::cout << "[WARN] pcAttackChars: NPC ID not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
BaseNPC* npc = NPCManager::NPCs[pktdata[i * 2]];
|
||||
if (npc->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] pcAttackChars: NPC is not a mob" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
Mob* mob = (Mob*)npc;
|
||||
|
||||
std::pair<int,int> damage;
|
||||
|
||||
if (pkt->iTargetCnt > 1)
|
||||
damage.first = plr->groupDamage;
|
||||
else
|
||||
damage.first = plr->pointDamage;
|
||||
|
||||
int difficulty = (int)mob->data["m_iNpcLevel"];
|
||||
|
||||
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
|
||||
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
|
||||
|
||||
if (plr->batteryW >= 6 + difficulty)
|
||||
plr->batteryW -= 6 + difficulty;
|
||||
else
|
||||
plr->batteryW = 0;
|
||||
|
||||
damage.first = hitMob(sock, mob, damage.first);
|
||||
|
||||
respdata[i].eCT = pktdata[i*2+1];
|
||||
respdata[i].iID = mob->appearanceData.iNPC_ID;
|
||||
respdata[i].iDamage = damage.first;
|
||||
respdata[i].iHP = mob->appearanceData.iHP;
|
||||
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
|
||||
}
|
||||
}
|
||||
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_CHARs_SUCC, resplen);
|
||||
|
||||
// a bit of a hack: these are the same size, so we can reuse the response packet
|
||||
assert(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) == sizeof(sP_FE2CL_PC_ATTACK_CHARs));
|
||||
sP_FE2CL_PC_ATTACK_CHARs *resp1 = (sP_FE2CL_PC_ATTACK_CHARs*)respbuf;
|
||||
|
||||
resp1->iPC_ID = plr->iID;
|
||||
|
||||
// send to other players
|
||||
PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_ATTACK_CHARs, resplen);
|
||||
}
|
||||
|
||||
static int8_t addBullet(Player* plr, bool isGrenade) {
|
||||
|
||||
int8_t findId = 0;
|
||||
if (Bullets.find(plr->iID) != Bullets.end()) {
|
||||
// find first free id
|
||||
for (; findId < 127; findId++)
|
||||
if (Bullets[plr->iID].find(findId) == Bullets[plr->iID].end())
|
||||
break;
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if (findId == 127) {
|
||||
std::cout << "[WARN] Player has more than 127 active projectiles?!" << std::endl;
|
||||
findId = 0;
|
||||
}
|
||||
|
||||
Bullet toAdd;
|
||||
toAdd.pointDamage = plr->pointDamage;
|
||||
toAdd.groupDamage = plr->groupDamage;
|
||||
// for grenade we need to send 1, for rocket - weapon id
|
||||
toAdd.bulletType = isGrenade ? 1 : plr->Equip[0].iID;
|
||||
|
||||
// temp solution Jade fix plz
|
||||
toAdd.weaponBoost = plr->batteryW > 0;
|
||||
if (toAdd.weaponBoost) {
|
||||
int boostCost = Rand::rand(11) + 20;
|
||||
plr->batteryW = boostCost > plr->batteryW ? 0 : plr->batteryW - boostCost;
|
||||
}
|
||||
|
||||
Bullets[plr->iID][findId] = toAdd;
|
||||
return findId;
|
||||
}
|
||||
|
||||
static void grenadeFire(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE* grenade = (sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, resp);
|
||||
resp.iToX = grenade->iToX;
|
||||
resp.iToY = grenade->iToY;
|
||||
resp.iToZ = grenade->iToZ;
|
||||
|
||||
resp.iBulletID = addBullet(plr, true);
|
||||
resp.iBatteryW = plr->batteryW;
|
||||
|
||||
// 1 means grenade
|
||||
resp.Bullet.iID = 1;
|
||||
sock->sendPacket(&resp, P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, sizeof(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC));
|
||||
|
||||
// send packet to nearby players
|
||||
INITSTRUCT(sP_FE2CL_PC_GRENADE_STYLE_FIRE, toOthers);
|
||||
toOthers.iPC_ID = plr->iID;
|
||||
toOthers.iToX = resp.iToX;
|
||||
toOthers.iToY = resp.iToY;
|
||||
toOthers.iToZ = resp.iToZ;
|
||||
toOthers.iBulletID = resp.iBulletID;
|
||||
toOthers.Bullet.iID = resp.Bullet.iID;
|
||||
|
||||
PlayerManager::sendToViewable(sock, &toOthers, P_FE2CL_PC_GRENADE_STYLE_FIRE, sizeof(sP_FE2CL_PC_GRENADE_STYLE_FIRE));
|
||||
}
|
||||
|
||||
static void rocketFire(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE* rocket = (sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// We should be sending back rocket succ packet, but it doesn't work, and this one works
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, resp);
|
||||
resp.iToX = rocket->iToX;
|
||||
resp.iToY = rocket->iToY;
|
||||
// rocket->iToZ is broken, this seems like a good height
|
||||
resp.iToZ = plr->z + 100;
|
||||
|
||||
resp.iBulletID = addBullet(plr, false);
|
||||
// we have to send it weapon id
|
||||
resp.Bullet.iID = plr->Equip[0].iID;
|
||||
resp.iBatteryW = plr->batteryW;
|
||||
|
||||
sock->sendPacket(&resp, P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, sizeof(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC));
|
||||
|
||||
// send packet to nearby players
|
||||
INITSTRUCT(sP_FE2CL_PC_GRENADE_STYLE_FIRE, toOthers);
|
||||
toOthers.iPC_ID = plr->iID;
|
||||
toOthers.iToX = resp.iToX;
|
||||
toOthers.iToY = resp.iToY;
|
||||
toOthers.iToZ = resp.iToZ;
|
||||
toOthers.iBulletID = resp.iBulletID;
|
||||
toOthers.Bullet.iID = resp.Bullet.iID;
|
||||
|
||||
PlayerManager::sendToViewable(sock, &toOthers, P_FE2CL_PC_GRENADE_STYLE_FIRE, sizeof(sP_FE2CL_PC_GRENADE_STYLE_FIRE));
|
||||
}
|
||||
|
||||
static void projectileHit(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT* pkt = (sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (pkt->iTargetCnt == 0) {
|
||||
Bullets[plr->iID].erase(pkt->iBulletID);
|
||||
// no targets hit, don't send response
|
||||
return;
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT), pkt->iTargetCnt, sizeof(int64_t), data->size)) {
|
||||
std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// client sends us 8 bytes, where last 4 bytes are mob ID,
|
||||
// we use int64 pointer to move around but have to remember to cast it to int32
|
||||
int64_t* pktdata = (int64_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT));
|
||||
|
||||
/*
|
||||
* Due to the possibility of multiplication overflow (and regular buffer overflow),
|
||||
* both incoming and outgoing variable-length packets must be validated, at least if
|
||||
* the number of trailing structs isn't well known (ie. it's from the client).
|
||||
*/
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT), pkt->iTargetCnt, sizeof(sAttackResult))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_PC_GRENADE_STYLE_HIT packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// rapid fire anti-cheat
|
||||
time_t currTime = getTime();
|
||||
if (currTime - plr->lastShot < plr->fireRate * 80)
|
||||
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
|
||||
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
|
||||
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing
|
||||
|
||||
plr->lastShot = currTime;
|
||||
|
||||
if (plr->suspicionRating > 10000) { // kill the socket when the player is too suspicious
|
||||
sock->kill();
|
||||
CNShardServer::_killConnection(sock);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* initialize response struct
|
||||
* rocket style hit doesn't work properly, so we're always sending this one
|
||||
*/
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT) + pkt->iTargetCnt * sizeof(sAttackResult);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_PC_GRENADE_STYLE_HIT* resp = (sP_FE2CL_PC_GRENADE_STYLE_HIT*)respbuf;
|
||||
sAttackResult* respdata = (sAttackResult*)(respbuf + sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT));
|
||||
|
||||
resp->iTargetCnt = pkt->iTargetCnt;
|
||||
if (Bullets.find(plr->iID) == Bullets.end() || Bullets[plr->iID].find(pkt->iBulletID) == Bullets[plr->iID].end()) {
|
||||
std::cout << "[WARN] projectileHit: bullet not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
Bullet* bullet = &Bullets[plr->iID][pkt->iBulletID];
|
||||
|
||||
for (int i = 0; i < pkt->iTargetCnt; i++) {
|
||||
if (NPCManager::NPCs.find(pktdata[i]) == NPCManager::NPCs.end()) {
|
||||
// not sure how to best handle this
|
||||
std::cout << "[WARN] projectileHit: NPC ID not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
BaseNPC* npc = NPCManager::NPCs[pktdata[i]];
|
||||
if (npc->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] projectileHit: NPC is not a mob" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
Mob* mob = (Mob*)npc;
|
||||
std::pair<int, int> damage;
|
||||
|
||||
damage.first = pkt->iTargetCnt > 1 ? bullet->groupDamage : bullet->pointDamage;
|
||||
|
||||
int difficulty = (int)mob->data["m_iNpcLevel"];
|
||||
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
|
||||
|
||||
damage.first = hitMob(sock, mob, damage.first);
|
||||
|
||||
respdata[i].iID = mob->appearanceData.iNPC_ID;
|
||||
respdata[i].iDamage = damage.first;
|
||||
respdata[i].iHP = mob->appearanceData.iHP;
|
||||
respdata[i].iHitFlag = damage.second;
|
||||
}
|
||||
|
||||
resp->iPC_ID = plr->iID;
|
||||
resp->iBulletID = pkt->iBulletID;
|
||||
resp->Bullet.iID = bullet->bulletType;
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_PC_GRENADE_STYLE_HIT, resplen);
|
||||
PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_GRENADE_STYLE_HIT, resplen);
|
||||
|
||||
Bullets[plr->iID].erase(resp->iBulletID);
|
||||
}
|
||||
|
||||
static void playerTick(CNServer *serv, time_t currTime) {
|
||||
static time_t lastHealTime = 0;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
CNSocket *sock = pair.first;
|
||||
Player *plr = pair.second;
|
||||
bool transmit = false;
|
||||
|
||||
// group ticks
|
||||
if (plr->groupCnt > 1)
|
||||
Groups::groupTickInfo(plr);
|
||||
|
||||
// do not tick dead players
|
||||
if (plr->HP <= 0)
|
||||
continue;
|
||||
|
||||
// fm patch/lake damage
|
||||
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION)
|
||||
&& !(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
|
||||
dealGooDamage(sock, PC_MAXHEALTH(plr->level) * 3 / 20);
|
||||
|
||||
// heal
|
||||
if (currTime - lastHealTime >= 4000 && !plr->inCombat && plr->HP < PC_MAXHEALTH(plr->level)) {
|
||||
if (currTime - lastHealTime - plr->healCooldown >= 4000) {
|
||||
plr->HP += PC_MAXHEALTH(plr->level) / 5;
|
||||
if (plr->HP > PC_MAXHEALTH(plr->level))
|
||||
plr->HP = PC_MAXHEALTH(plr->level);
|
||||
transmit = true;
|
||||
} else
|
||||
plr->healCooldown -= 4000;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (plr->activeNano != 0 && plr->equippedNanos[i] == plr->activeNano) { // spend stamina
|
||||
plr->Nanos[plr->activeNano].iStamina -= 1 + plr->nanoDrainRate / 5;
|
||||
|
||||
if (plr->Nanos[plr->activeNano].iStamina <= 0)
|
||||
Nanos::summonNano(sock, -1, true); // unsummon nano silently
|
||||
|
||||
transmit = true;
|
||||
} else if (plr->Nanos[plr->equippedNanos[i]].iStamina < 150) { // regain stamina
|
||||
sNano& nano = plr->Nanos[plr->equippedNanos[i]];
|
||||
nano.iStamina += 1;
|
||||
|
||||
if (nano.iStamina > 150)
|
||||
nano.iStamina = 150;
|
||||
|
||||
transmit = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the player has fallen out of the world
|
||||
if (plr->z < -30000) {
|
||||
INITSTRUCT(sP_FE2CL_PC_SUDDEN_DEAD, dead);
|
||||
|
||||
dead.iPC_ID = plr->iID;
|
||||
dead.iDamage = plr->HP;
|
||||
dead.iHP = plr->HP = 0;
|
||||
|
||||
sock->sendPacket((void*)&dead, P_FE2CL_PC_SUDDEN_DEAD, sizeof(sP_FE2CL_PC_SUDDEN_DEAD));
|
||||
PlayerManager::sendToViewable(sock, (void*)&dead, P_FE2CL_PC_SUDDEN_DEAD, sizeof(sP_FE2CL_PC_SUDDEN_DEAD));
|
||||
}
|
||||
|
||||
if (transmit) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TICK, pkt);
|
||||
|
||||
pkt.iHP = plr->HP;
|
||||
pkt.iBatteryN = plr->batteryN;
|
||||
|
||||
pkt.aNano[0] = plr->Nanos[plr->equippedNanos[0]];
|
||||
pkt.aNano[1] = plr->Nanos[plr->equippedNanos[1]];
|
||||
pkt.aNano[2] = plr->Nanos[plr->equippedNanos[2]];
|
||||
|
||||
sock->sendPacket((void*)&pkt, P_FE2CL_REP_PC_TICK, sizeof(sP_FE2CL_REP_PC_TICK));
|
||||
}
|
||||
}
|
||||
|
||||
// if this was a heal tick, update the counter outside of the loop
|
||||
if (currTime - lastHealTime >= 4000)
|
||||
lastHealTime = currTime;
|
||||
}
|
||||
|
||||
void Combat::init() {
|
||||
REGISTER_SHARD_TIMER(playerTick, 2000);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, pcAttackNpcs);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_COMBAT_BEGIN, combatBegin);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_COMBAT_END, combatEnd);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_DOT_DAMAGE_ONOFF, dotDamageOnOff);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_CHARs, pcAttackChars);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE, grenadeFire);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE, rocketFire);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_HIT, projectileHit);
|
||||
}
|
||||
29
src/Combat.hpp
Normal file
29
src/Combat.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "NPC.hpp"
|
||||
#include "MobAI.hpp"
|
||||
|
||||
#include "JSON.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <queue>
|
||||
|
||||
struct Bullet {
|
||||
int pointDamage;
|
||||
int groupDamage;
|
||||
bool weaponBoost;
|
||||
int bulletType;
|
||||
};
|
||||
|
||||
namespace Combat {
|
||||
extern std::map<int32_t, std::map<int8_t, Bullet>> Bullets;
|
||||
|
||||
void init();
|
||||
|
||||
void npcAttackPc(Mob *mob, time_t currTime);
|
||||
int hitMob(CNSocket *sock, Mob *mob, int damage);
|
||||
void killMob(CNSocket *sock, Mob *mob);
|
||||
}
|
||||
1222
src/CustomCommands.cpp
Normal file
1222
src/CustomCommands.cpp
Normal file
File diff suppressed because it is too large
Load Diff
9
src/CustomCommands.hpp
Normal file
9
src/CustomCommands.hpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
|
||||
namespace CustomCommands {
|
||||
void init();
|
||||
|
||||
bool runCmd(std::string full, CNSocket* sock);
|
||||
};
|
||||
266
src/Eggs.cpp
Normal file
266
src/Eggs.cpp
Normal file
@@ -0,0 +1,266 @@
|
||||
#include "core/Core.hpp"
|
||||
#include "Eggs.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "Abilities.hpp"
|
||||
#include "Groups.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
using namespace Eggs;
|
||||
|
||||
/// sock, CBFlag -> until
|
||||
std::map<std::pair<CNSocket*, int32_t>, time_t> Eggs::EggBuffs;
|
||||
std::unordered_map<int, EggType> Eggs::EggTypes;
|
||||
|
||||
int Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
||||
|
||||
int bitFlag = Groups::getGroupFlags(otherPlr);
|
||||
int CBFlag = Nanos::applyBuff(sock, skillId, 1, 3, bitFlag);
|
||||
|
||||
size_t resplen;
|
||||
|
||||
if (skillId == 183) {
|
||||
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Damage);
|
||||
} else if (skillId == 150) {
|
||||
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Heal_HP);
|
||||
} else {
|
||||
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Buff);
|
||||
}
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
auto skillUse = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
|
||||
|
||||
if (skillId == 183) { // damage egg
|
||||
auto skill = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
|
||||
memset(respbuf, 0, resplen);
|
||||
skill->eCT = 1;
|
||||
skill->iID = plr->iID;
|
||||
skill->iDamage = PC_MAXHEALTH(plr->level) * Nanos::SkillTable[skillId].powerIntensity[0] / 1000;
|
||||
plr->HP -= skill->iDamage;
|
||||
if (plr->HP < 0)
|
||||
plr->HP = 0;
|
||||
skill->iHP = plr->HP;
|
||||
} else if (skillId == 150) { // heal egg
|
||||
auto skill = (sSkillResult_Heal_HP*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
|
||||
memset(respbuf, 0, resplen);
|
||||
skill->eCT = 1;
|
||||
skill->iID = plr->iID;
|
||||
skill->iHealHP = PC_MAXHEALTH(plr->level) * Nanos::SkillTable[skillId].powerIntensity[0] / 1000;
|
||||
plr->HP += skill->iHealHP;
|
||||
if (plr->HP > PC_MAXHEALTH(plr->level))
|
||||
plr->HP = PC_MAXHEALTH(plr->level);
|
||||
skill->iHP = plr->HP;
|
||||
} else { // regular buff egg
|
||||
auto skill = (sSkillResult_Buff*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
|
||||
memset(respbuf, 0, resplen);
|
||||
skill->eCT = 1;
|
||||
skill->iID = plr->iID;
|
||||
skill->iConditionBitFlag = plr->iConditionBitFlag;
|
||||
}
|
||||
|
||||
skillUse->iNPC_ID = eggId;
|
||||
skillUse->iSkillID = skillId;
|
||||
skillUse->eST = Nanos::SkillTable[skillId].skillType;
|
||||
skillUse->iTargetCnt = 1;
|
||||
|
||||
sock->sendPacket((void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
|
||||
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
|
||||
|
||||
if (CBFlag == 0)
|
||||
return -1;
|
||||
|
||||
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, CBFlag);
|
||||
|
||||
// save the buff serverside;
|
||||
// if you get the same buff again, new duration will override the previous one
|
||||
time_t until = getTime() + (time_t)duration * 1000;
|
||||
EggBuffs[key] = until;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void eggStep(CNServer* serv, time_t currTime) {
|
||||
// tick buffs
|
||||
time_t timeStamp = currTime;
|
||||
auto it = EggBuffs.begin();
|
||||
while (it != EggBuffs.end()) {
|
||||
// check remaining time
|
||||
if (it->second > timeStamp) {
|
||||
it++;
|
||||
} else { // if time reached 0
|
||||
CNSocket* sock = it->first.first;
|
||||
int32_t CBFlag = it->first.second;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
||||
|
||||
int groupFlags = Groups::getGroupFlags(otherPlr);
|
||||
for (auto& pwr : Nanos::NanoPowers) {
|
||||
if (pwr.bitFlag == CBFlag) { // pick the power with the right flag and unbuff
|
||||
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp);
|
||||
resp.eCSTB = pwr.timeBuffID;
|
||||
resp.eTBU = 2;
|
||||
resp.eTBT = 3; // for egg buffs
|
||||
plr->iConditionBitFlag &= ~CBFlag;
|
||||
resp.iConditionBitFlag = plr->iConditionBitFlag |= groupFlags | plr->iSelfConditionBitFlag;
|
||||
sock->sendPacket(resp, P_FE2CL_PC_BUFF_UPDATE);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, resp2); // send a buff timeout to other players
|
||||
resp2.eCT = 1;
|
||||
resp2.iID = plr->iID;
|
||||
resp2.iConditionBitFlag = plr->iConditionBitFlag;
|
||||
PlayerManager::sendToViewable(sock, resp2, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT);
|
||||
}
|
||||
}
|
||||
// remove buff from the map
|
||||
it = EggBuffs.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// check dead eggs and eggs in inactive chunks
|
||||
for (auto npc : NPCManager::NPCs) {
|
||||
if (npc.second->type != EntityType::EGG)
|
||||
continue;
|
||||
|
||||
auto egg = (Egg*)npc.second;
|
||||
if (!egg->dead || !Chunking::inPopulatedChunks(&egg->viewableChunks))
|
||||
continue;
|
||||
|
||||
if (egg->deadUntil <= timeStamp) {
|
||||
// respawn it
|
||||
egg->dead = false;
|
||||
egg->deadUntil = 0;
|
||||
egg->appearanceData.iHP = 400;
|
||||
|
||||
Chunking::addEntityToChunks(Chunking::getViewableChunks(egg->chunkPos), {npc.first});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Eggs::npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg) {
|
||||
egg->iX = x;
|
||||
egg->iY = y;
|
||||
egg->iZ = z;
|
||||
// client doesn't care about egg->iMapNum
|
||||
egg->iShinyType = npc->iNPCType;
|
||||
egg->iShiny_ID = npc->iNPC_ID;
|
||||
}
|
||||
|
||||
static void eggPickup(CNSocket* sock, CNPacketData* data) {
|
||||
auto pickup = (sP_CL2FE_REQ_SHINY_PICKUP*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
EntityRef eggRef = {pickup->iShinyID};
|
||||
|
||||
if (!eggRef.isValid()) {
|
||||
std::cout << "[WARN] Player tried to open non existing egg?!" << std::endl;
|
||||
return;
|
||||
}
|
||||
auto egg = (Egg*)eggRef.getEntity();
|
||||
if (egg->type != EntityType::EGG) {
|
||||
std::cout << "[WARN] Player tried to open something other than an?!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (egg->dead) {
|
||||
std::cout << "[WARN] Player tried to open a dead egg?!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
/* this has some issues with position desync, leaving it out for now
|
||||
if (abs(egg->x - plr->x)>500 || abs(egg->y - plr->y) > 500) {
|
||||
std::cout << "[WARN] Player tried to open an egg isn't nearby?!" << std::endl;
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
int typeId = egg->appearanceData.iNPCType;
|
||||
if (EggTypes.find(typeId) == EggTypes.end()) {
|
||||
std::cout << "[WARN] Egg Type " << typeId << " not found!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
EggType* type = &EggTypes[typeId];
|
||||
|
||||
// buff the player
|
||||
if (type->effectId != 0)
|
||||
eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration);
|
||||
|
||||
/*
|
||||
* SHINY_PICKUP_SUCC is only causing a GUI effect in the client
|
||||
* (buff icon pops up in the bottom of the screen)
|
||||
* so we don't send it for non-effect
|
||||
*/
|
||||
if (type->effectId != 0) {
|
||||
INITSTRUCT(sP_FE2CL_REP_SHINY_PICKUP_SUCC, resp);
|
||||
resp.iSkillID = type->effectId;
|
||||
|
||||
// in general client finds correct icon on it's own,
|
||||
// but for damage we have to supply correct CSTB
|
||||
if (resp.iSkillID == 183)
|
||||
resp.eCSTB = ECSB_INFECTION;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_SHINY_PICKUP_SUCC);
|
||||
}
|
||||
|
||||
// drop
|
||||
if (type->dropCrateId != 0) {
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
sP_FE2CL_REP_REWARD_ITEM* reward = (sP_FE2CL_REP_REWARD_ITEM*)respbuf;
|
||||
sItemReward* item = (sItemReward*)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||
|
||||
// don't forget to zero the buffer!
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
// send back player's stats
|
||||
reward->m_iCandy = plr->money;
|
||||
reward->m_iFusionMatter = plr->fusionmatter;
|
||||
reward->m_iBatteryN = plr->batteryN;
|
||||
reward->m_iBatteryW = plr->batteryW;
|
||||
reward->iFatigue = 100; // prevents warning message
|
||||
reward->iFatigue_Level = 1;
|
||||
reward->iItemCnt = 1; // remember to update resplen if you change this
|
||||
|
||||
int slot = Items::findFreeSlot(plr);
|
||||
|
||||
// no space for drop
|
||||
if (slot != -1) {
|
||||
|
||||
// item reward
|
||||
item->sItem.iType = 9;
|
||||
item->sItem.iOpt = 1;
|
||||
item->sItem.iID = type->dropCrateId;
|
||||
item->iSlotNum = slot;
|
||||
item->eIL = 1; // Inventory Location. 1 means player inventory.
|
||||
|
||||
// update player
|
||||
plr->Inven[slot] = item->sItem;
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||
}
|
||||
}
|
||||
|
||||
if (egg->summoned)
|
||||
NPCManager::destroyNPC(eggRef.id);
|
||||
else {
|
||||
Chunking::removeEntityFromChunks(Chunking::getViewableChunks(egg->chunkPos), eggRef);
|
||||
egg->dead = true;
|
||||
egg->deadUntil = getTime() + (time_t)type->regen * 1000;
|
||||
egg->appearanceData.iHP = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Eggs::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SHINY_PICKUP, eggPickup);
|
||||
|
||||
REGISTER_SHARD_TIMER(eggStep, 1000);
|
||||
}
|
||||
22
src/Eggs.hpp
Normal file
22
src/Eggs.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "Entities.hpp"
|
||||
|
||||
struct EggType {
|
||||
int dropCrateId;
|
||||
int effectId;
|
||||
int duration;
|
||||
int regen;
|
||||
};
|
||||
|
||||
namespace Eggs {
|
||||
extern std::map<std::pair<CNSocket*, int32_t>, time_t> EggBuffs;
|
||||
extern std::unordered_map<int, EggType> EggTypes;
|
||||
|
||||
void init();
|
||||
|
||||
/// returns -1 on fail
|
||||
int eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration);
|
||||
void npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg);
|
||||
}
|
||||
339
src/Email.cpp
Normal file
339
src/Email.cpp
Normal file
@@ -0,0 +1,339 @@
|
||||
#include "Email.hpp"
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "db/Database.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "Chat.hpp"
|
||||
|
||||
using namespace Email;
|
||||
|
||||
std::vector<std::string> Email::dump;
|
||||
|
||||
// New email notification
|
||||
static void emailUpdateCheck(CNSocket* sock, CNPacketData* data) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_NEW_EMAIL, resp);
|
||||
resp.iNewEmailCnt = Database::getUnreadEmailCount(PlayerManager::getPlayer(sock)->iID);
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_NEW_EMAIL);
|
||||
}
|
||||
|
||||
// Retrieve page of emails
|
||||
static void emailReceivePageList(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST*)data->buf;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_SUCC, resp);
|
||||
resp.iPageNum = pkt->iPageNum;
|
||||
|
||||
std::vector<Database::EmailData> emails = Database::getEmails(PlayerManager::getPlayer(sock)->iID, pkt->iPageNum);
|
||||
for (int i = 0; i < emails.size(); i++) {
|
||||
// convert each email and load them into the packet
|
||||
Database::EmailData* email = &emails.at(i);
|
||||
sEmailInfo* emailInfo = new sEmailInfo();
|
||||
emailInfo->iEmailIndex = email->MsgIndex;
|
||||
emailInfo->iReadFlag = email->ReadFlag;
|
||||
emailInfo->iItemCandyFlag = email->ItemFlag;
|
||||
emailInfo->iFromPCUID = email->SenderId;
|
||||
emailInfo->SendTime = timeStampToStruct(email->SendTime);
|
||||
emailInfo->DeleteTime = timeStampToStruct(email->DeleteTime);
|
||||
U8toU16(email->SenderFirstName, emailInfo->szFirstName, sizeof(emailInfo->szFirstName));
|
||||
U8toU16(email->SenderLastName, emailInfo->szLastName, sizeof(emailInfo->szLastName));
|
||||
U8toU16(email->SubjectLine, emailInfo->szSubject, sizeof(emailInfo->szSubject));
|
||||
resp.aEmailInfo[i] = *emailInfo;
|
||||
}
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_SUCC);
|
||||
}
|
||||
|
||||
// Read individual email
|
||||
static void emailRead(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_PC_READ_EMAIL*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex);
|
||||
sItemBase* attachments = Database::getEmailAttachments(plr->iID, pkt->iEmailIndex);
|
||||
email.ReadFlag = 1; // mark as read
|
||||
Database::updateEmailContent(&email);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_READ_EMAIL_SUCC, resp);
|
||||
resp.iEmailIndex = pkt->iEmailIndex;
|
||||
resp.iCash = email.Taros;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
resp.aItem[i] = attachments[i];
|
||||
}
|
||||
U8toU16(email.MsgBody, (char16_t*)resp.szContent, sizeof(resp.szContent));
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_READ_EMAIL_SUCC);
|
||||
}
|
||||
|
||||
// Retrieve attached taros from email
|
||||
static void emailReceiveTaros(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_CANDY*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex);
|
||||
// money transfer
|
||||
plr->money += email.Taros;
|
||||
email.Taros = 0;
|
||||
// update Taros in email
|
||||
Database::updateEmailContent(&email);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_RECV_EMAIL_CANDY_SUCC, resp);
|
||||
resp.iCandy = plr->money;
|
||||
resp.iEmailIndex = pkt->iEmailIndex;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_RECV_EMAIL_CANDY_SUCC);
|
||||
}
|
||||
|
||||
// Retrieve individual attached item from email
|
||||
static void emailReceiveItemSingle(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT || pkt->iEmailItemSlot < 1 || pkt->iEmailItemSlot > 4)
|
||||
return; // sanity check
|
||||
|
||||
// get email item from db and delete it
|
||||
sItemBase* attachments = Database::getEmailAttachments(plr->iID, pkt->iEmailIndex);
|
||||
sItemBase itemFrom = attachments[pkt->iEmailItemSlot - 1];
|
||||
Database::deleteEmailAttachments(plr->iID, pkt->iEmailIndex, pkt->iEmailItemSlot);
|
||||
|
||||
// move item to player inventory
|
||||
sItemBase& itemTo = plr->Inven[pkt->iSlotNum];
|
||||
itemTo.iID = itemFrom.iID;
|
||||
itemTo.iOpt = itemFrom.iOpt;
|
||||
itemTo.iTimeLimit = itemFrom.iTimeLimit;
|
||||
itemTo.iType = itemFrom.iType;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_RECV_EMAIL_ITEM_SUCC, resp);
|
||||
resp.iEmailIndex = pkt->iEmailIndex;
|
||||
resp.iEmailItemSlot = pkt->iEmailItemSlot;
|
||||
resp.iSlotNum = pkt->iSlotNum;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_RECV_EMAIL_ITEM_SUCC);
|
||||
|
||||
// update inventory
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp2);
|
||||
resp2.eIL = 1;
|
||||
resp2.iSlotNum = resp.iSlotNum;
|
||||
resp2.Item = itemTo;
|
||||
|
||||
sock->sendPacket(resp2, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
|
||||
}
|
||||
|
||||
// Retrieve all attached items from email
|
||||
static void emailReceiveItemAll(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL*)data->buf;
|
||||
|
||||
// move items to player inventory
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
sItemBase* itemsFrom = Database::getEmailAttachments(plr->iID, pkt->iEmailIndex);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int slot = Items::findFreeSlot(plr);
|
||||
if (slot < 0 || slot >= AINVEN_COUNT) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_FAIL, failResp);
|
||||
failResp.iEmailIndex = pkt->iEmailIndex;
|
||||
failResp.iErrorCode = 0; // ???
|
||||
break; // sanity check; should never happen
|
||||
}
|
||||
|
||||
// copy data over
|
||||
sItemBase itemFrom = itemsFrom[i];
|
||||
sItemBase& itemTo = plr->Inven[slot];
|
||||
itemTo.iID = itemFrom.iID;
|
||||
itemTo.iOpt = itemFrom.iOpt;
|
||||
itemTo.iTimeLimit = itemFrom.iTimeLimit;
|
||||
itemTo.iType = itemFrom.iType;
|
||||
|
||||
// update inventory
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp2);
|
||||
resp2.eIL = 1;
|
||||
resp2.iSlotNum = slot;
|
||||
resp2.Item = itemTo;
|
||||
|
||||
sock->sendPacket(resp2, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
|
||||
}
|
||||
|
||||
// delete all items from db
|
||||
Database::deleteEmailAttachments(plr->iID, pkt->iEmailIndex, -1);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_SUCC, resp);
|
||||
resp.iEmailIndex = pkt->iEmailIndex;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_SUCC);
|
||||
}
|
||||
|
||||
// Delete an email
|
||||
static void emailDelete(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_PC_DELETE_EMAIL*)data->buf;
|
||||
|
||||
Database::deleteEmails(PlayerManager::getPlayer(sock)->iID, pkt->iEmailIndexArray);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_DELETE_EMAIL_SUCC, resp);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
resp.iEmailIndexArray[i] = pkt->iEmailIndexArray[i]; // i'm scared of memcpy
|
||||
}
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_DELETE_EMAIL_SUCC);
|
||||
}
|
||||
|
||||
// Send an email
|
||||
static void emailSend(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_PC_SEND_EMAIL*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// sanity checks
|
||||
bool invalid = false;
|
||||
int itemCount = 0;
|
||||
|
||||
std::set<int> seen;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int slot = pkt->aItem[i].iSlotNum;
|
||||
if (slot < 0 || slot >= AINVEN_COUNT) {
|
||||
invalid = true;
|
||||
break;
|
||||
}
|
||||
|
||||
sItemBase* item = &pkt->aItem[i].ItemInven;
|
||||
sItemBase* real = &plr->Inven[slot];
|
||||
|
||||
if (item->iID == 0)
|
||||
continue;
|
||||
|
||||
// was the same item added multiple times?
|
||||
if (seen.count(slot) > 0) {
|
||||
invalid = true;
|
||||
break;
|
||||
}
|
||||
seen.insert(slot);
|
||||
|
||||
itemCount++;
|
||||
if (item->iType != real->iType || item->iID != real->iID
|
||||
|| item->iOpt <= 0 || item->iOpt > real->iOpt) {
|
||||
invalid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pkt->iCash < 0 || pkt->iCash > plr->money + 50 + 20 * itemCount || invalid) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_FAIL, errResp);
|
||||
errResp.iErrorCode = 1;
|
||||
errResp.iTo_PCUID = pkt->iTo_PCUID;
|
||||
sock->sendPacket(errResp, P_FE2CL_REP_PC_SEND_EMAIL_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_SUCC, resp);
|
||||
|
||||
Player otherPlr = {};
|
||||
Database::getPlayer(&otherPlr, pkt->iTo_PCUID);
|
||||
if (pkt->iCash || pkt->aItem[0].ItemInven.iID) {
|
||||
// if there are item or taro attachments
|
||||
if (otherPlr.iID != 0 && plr->PCStyle2.iPayzoneFlag != otherPlr.PCStyle2.iPayzoneFlag) {
|
||||
// if the players are not in the same time period
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_FAIL, resp);
|
||||
resp.iErrorCode = 9; // error code 9 tells the player they can't send attachments across time
|
||||
resp.iTo_PCUID = pkt->iTo_PCUID;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_SEND_EMAIL_FAIL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// handle items
|
||||
std::vector<sItemBase> attachments;
|
||||
std::vector<int> attSlots;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
sEmailItemInfoFromCL attachment = pkt->aItem[i];
|
||||
|
||||
// skip empty slots
|
||||
if (attachment.ItemInven.iID == 0)
|
||||
continue;
|
||||
|
||||
sItemBase* item = &pkt->aItem[i].ItemInven;
|
||||
sItemBase* real = &plr->Inven[attachment.iSlotNum];
|
||||
|
||||
resp.aItem[i] = attachment;
|
||||
attachments.push_back(attachment.ItemInven);
|
||||
attSlots.push_back(attachment.iSlotNum);
|
||||
if (real->iOpt <= item->iOpt) // delete item (if they attached the whole stack)
|
||||
*real = { 0, 0, 0, 0 };
|
||||
else // otherwise, decrement the item
|
||||
real->iOpt -= item->iOpt;
|
||||
|
||||
// HACK: update the slot
|
||||
INITSTRUCT(sP_FE2CL_PC_ITEM_MOVE_SUCC, itemResp);
|
||||
itemResp.iFromSlotNum = attachment.iSlotNum;
|
||||
itemResp.iToSlotNum = attachment.iSlotNum;
|
||||
itemResp.FromSlotItem = *real;
|
||||
itemResp.ToSlotItem = *real;
|
||||
itemResp.eFrom = (int32_t)Items::SlotType::INVENTORY;
|
||||
itemResp.eTo = (int32_t)Items::SlotType::INVENTORY;
|
||||
sock->sendPacket(itemResp, P_FE2CL_PC_ITEM_MOVE_SUCC);
|
||||
}
|
||||
|
||||
int cost = pkt->iCash + 50 + 20 * attachments.size(); // attached taros + postage
|
||||
plr->money -= cost;
|
||||
Database::EmailData email = {
|
||||
(int)pkt->iTo_PCUID, // PlayerId
|
||||
Database::getNextEmailIndex(pkt->iTo_PCUID), // MsgIndex
|
||||
0, // ReadFlag (unread)
|
||||
(pkt->iCash > 0 || attachments.size() > 0) ? 1 : 0, // ItemFlag
|
||||
plr->iID, // SenderID
|
||||
AUTOU16TOU8(plr->PCStyle.szFirstName), // SenderFirstName
|
||||
AUTOU16TOU8(plr->PCStyle.szLastName), // SenderLastName
|
||||
Chat::sanitizeText(AUTOU16TOU8(pkt->szSubject)), // SubjectLine
|
||||
Chat::sanitizeText(AUTOU16TOU8(pkt->szContent), true), // MsgBody
|
||||
pkt->iCash, // Taros
|
||||
(uint64_t)getTimestamp(), // SendTime
|
||||
0 // DeleteTime (unimplemented)
|
||||
};
|
||||
|
||||
if (!Database::sendEmail(&email, attachments, plr)) {
|
||||
plr->money += cost; // give money back
|
||||
// give items back
|
||||
while (!attachments.empty()) {
|
||||
sItemBase attachment = attachments.back();
|
||||
plr->Inven[attSlots.back()] = attachment;
|
||||
|
||||
attachments.pop_back();
|
||||
attSlots.pop_back();
|
||||
}
|
||||
|
||||
// send error message
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_FAIL, errResp);
|
||||
errResp.iErrorCode = 1;
|
||||
errResp.iTo_PCUID = pkt->iTo_PCUID;
|
||||
sock->sendPacket(errResp, P_FE2CL_REP_PC_SEND_EMAIL_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// HACK: use set value packet to force GUI taros update
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_SET_VALUE, tarosResp);
|
||||
tarosResp.iPC_ID = plr->iID;
|
||||
tarosResp.iSetValueType = 5;
|
||||
tarosResp.iSetValue = plr->money;
|
||||
sock->sendPacket(tarosResp, P_FE2CL_GM_REP_PC_SET_VALUE);
|
||||
|
||||
resp.iCandy = plr->money;
|
||||
resp.iTo_PCUID = pkt->iTo_PCUID;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_SEND_EMAIL_SUCC);
|
||||
|
||||
std::string logEmail = "[Email] " + PlayerManager::getPlayerName(plr, true) + " (to " + PlayerManager::getPlayerName(&otherPlr, true) + "): <" + email.SubjectLine + ">\n" + email.MsgBody;
|
||||
std::cout << logEmail << std::endl;
|
||||
dump.push_back(logEmail);
|
||||
}
|
||||
|
||||
void Email::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_EMAIL_UPDATE_CHECK, emailUpdateCheck);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST, emailReceivePageList);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_READ_EMAIL, emailRead);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_CANDY, emailReceiveTaros);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM, emailReceiveItemSingle);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL, emailReceiveItemAll);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_DELETE_EMAIL, emailDelete);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SEND_EMAIL, emailSend);
|
||||
}
|
||||
10
src/Email.hpp
Normal file
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();
|
||||
}
|
||||
120
src/Entities.cpp
Normal file
120
src/Entities.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "core/Core.hpp"
|
||||
#include "Entities.hpp"
|
||||
#include "Chunking.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
#include "Eggs.hpp"
|
||||
#include "MobAI.hpp"
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
static_assert(std::is_standard_layout<EntityRef>::value);
|
||||
static_assert(std::is_trivially_copyable<EntityRef>::value);
|
||||
|
||||
EntityRef::EntityRef(CNSocket *s) {
|
||||
type = EntityType::PLAYER;
|
||||
sock = s;
|
||||
}
|
||||
|
||||
EntityRef::EntityRef(int32_t i) {
|
||||
id = i;
|
||||
|
||||
assert(NPCManager::NPCs.find(id) != NPCManager::NPCs.end());
|
||||
type = NPCManager::NPCs[id]->type;
|
||||
}
|
||||
|
||||
bool EntityRef::isValid() const {
|
||||
if (type == EntityType::PLAYER)
|
||||
return PlayerManager::players.find(sock) != PlayerManager::players.end();
|
||||
|
||||
return NPCManager::NPCs.find(id) != NPCManager::NPCs.end();
|
||||
}
|
||||
|
||||
Entity *EntityRef::getEntity() const {
|
||||
assert(isValid());
|
||||
|
||||
if (type == EntityType::PLAYER)
|
||||
return PlayerManager::getPlayer(sock);
|
||||
|
||||
return NPCManager::NPCs[id];
|
||||
}
|
||||
|
||||
/*
|
||||
* Entity coming into view.
|
||||
*/
|
||||
void BaseNPC::enterIntoViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
|
||||
pkt.NPCAppearanceData = appearanceData;
|
||||
pkt.NPCAppearanceData.iX = x;
|
||||
pkt.NPCAppearanceData.iY = y;
|
||||
pkt.NPCAppearanceData.iZ = z;
|
||||
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
|
||||
}
|
||||
|
||||
void Bus::enterIntoViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_TRANSPORTATION_ENTER, pkt);
|
||||
|
||||
// TODO: Potentially decouple this from BaseNPC?
|
||||
pkt.AppearanceData = {
|
||||
3, appearanceData.iNPC_ID, appearanceData.iNPCType,
|
||||
x, y, z
|
||||
};
|
||||
|
||||
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_ENTER);
|
||||
}
|
||||
|
||||
void Egg::enterIntoViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt);
|
||||
|
||||
Eggs::npcDataToEggData(x, y, z, &appearanceData, &pkt.ShinyAppearanceData);
|
||||
|
||||
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER);
|
||||
}
|
||||
|
||||
// TODO: this is less effiecient than it was, because of memset()
|
||||
void Player::enterIntoViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_PC_NEW, pkt);
|
||||
|
||||
pkt.PCAppearanceData.iID = iID;
|
||||
pkt.PCAppearanceData.iHP = HP;
|
||||
pkt.PCAppearanceData.iLv = level;
|
||||
pkt.PCAppearanceData.iX = x;
|
||||
pkt.PCAppearanceData.iY = y;
|
||||
pkt.PCAppearanceData.iZ = z;
|
||||
pkt.PCAppearanceData.iAngle = angle;
|
||||
pkt.PCAppearanceData.PCStyle = PCStyle;
|
||||
pkt.PCAppearanceData.Nano = Nanos[activeNano];
|
||||
pkt.PCAppearanceData.iPCState = iPCState;
|
||||
pkt.PCAppearanceData.iSpecialState = iSpecialState;
|
||||
memcpy(pkt.PCAppearanceData.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT);
|
||||
|
||||
sock->sendPacket(pkt, P_FE2CL_PC_NEW);
|
||||
}
|
||||
|
||||
/*
|
||||
* Entity leaving view.
|
||||
*/
|
||||
void BaseNPC::disappearFromViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
|
||||
pkt.iNPC_ID = appearanceData.iNPC_ID;
|
||||
sock->sendPacket(pkt, P_FE2CL_NPC_EXIT);
|
||||
}
|
||||
|
||||
void Bus::disappearFromViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_TRANSPORTATION_EXIT, pkt);
|
||||
pkt.eTT = 3;
|
||||
pkt.iT_ID = appearanceData.iNPC_ID;
|
||||
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_EXIT);
|
||||
}
|
||||
|
||||
void Egg::disappearFromViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_SHINY_EXIT, pkt);
|
||||
pkt.iShinyID = appearanceData.iNPC_ID;
|
||||
sock->sendPacket(pkt, P_FE2CL_SHINY_EXIT);
|
||||
}
|
||||
|
||||
void Player::disappearFromViewOf(CNSocket *sock) {
|
||||
INITSTRUCT(sP_FE2CL_PC_EXIT, pkt);
|
||||
pkt.iID = iID;
|
||||
sock->sendPacket(pkt, P_FE2CL_PC_EXIT);
|
||||
}
|
||||
151
src/Entities.hpp
Normal file
151
src/Entities.hpp
Normal file
@@ -0,0 +1,151 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "Chunking.hpp"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <set>
|
||||
|
||||
enum class EntityType : uint8_t {
|
||||
INVALID,
|
||||
PLAYER,
|
||||
SIMPLE_NPC,
|
||||
COMBAT_NPC,
|
||||
MOB,
|
||||
EGG,
|
||||
BUS
|
||||
};
|
||||
|
||||
struct Entity {
|
||||
EntityType type = EntityType::INVALID;
|
||||
int x = 0, y = 0, z = 0;
|
||||
uint64_t instanceID = 0;
|
||||
ChunkPos chunkPos = {};
|
||||
std::set<Chunk*> viewableChunks = {};
|
||||
|
||||
// destructor must be virtual, apparently
|
||||
virtual ~Entity() {}
|
||||
|
||||
virtual bool isAlive() { return true; }
|
||||
|
||||
// stubs
|
||||
virtual void enterIntoViewOf(CNSocket *sock) = 0;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) = 0;
|
||||
};
|
||||
|
||||
struct EntityRef {
|
||||
EntityType type;
|
||||
union {
|
||||
CNSocket *sock;
|
||||
int32_t id;
|
||||
};
|
||||
|
||||
EntityRef(CNSocket *s);
|
||||
EntityRef(int32_t i);
|
||||
|
||||
bool isValid() const;
|
||||
Entity *getEntity() const;
|
||||
|
||||
bool operator==(const EntityRef& other) const {
|
||||
if (type != other.type)
|
||||
return false;
|
||||
|
||||
if (type == EntityType::PLAYER)
|
||||
return sock == other.sock;
|
||||
|
||||
return id == other.id;
|
||||
}
|
||||
|
||||
// arbitrary ordering
|
||||
bool operator<(const EntityRef& other) const {
|
||||
if (type == other.type) {
|
||||
if (type == EntityType::PLAYER)
|
||||
return sock < other.sock;
|
||||
else
|
||||
return id < other.id;
|
||||
}
|
||||
|
||||
return type < other.type;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Subclasses
|
||||
*/
|
||||
class BaseNPC : public Entity {
|
||||
public:
|
||||
sNPCAppearanceData appearanceData = {};
|
||||
bool loopingPath = false;
|
||||
|
||||
BaseNPC(int _X, int _Y, int _Z, int angle, uint64_t iID, int t, int id) { // XXX
|
||||
x = _X;
|
||||
y = _Y;
|
||||
z = _Z;
|
||||
appearanceData.iNPCType = t;
|
||||
appearanceData.iHP = 400;
|
||||
appearanceData.iAngle = angle;
|
||||
appearanceData.iConditionBitFlag = 0;
|
||||
appearanceData.iBarkerType = 0;
|
||||
appearanceData.iNPC_ID = id;
|
||||
|
||||
instanceID = iID;
|
||||
};
|
||||
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
};
|
||||
|
||||
struct CombatNPC : public BaseNPC {
|
||||
int maxHealth = 0;
|
||||
int spawnX = 0;
|
||||
int spawnY = 0;
|
||||
int spawnZ = 0;
|
||||
int level = 0;
|
||||
int speed = 300;
|
||||
|
||||
void (*_stepAI)(CombatNPC*, time_t) = nullptr;
|
||||
|
||||
// XXX
|
||||
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP) :
|
||||
BaseNPC(x, y, z, angle, iID, t, id),
|
||||
maxHealth(maxHP) {}
|
||||
|
||||
virtual void stepAI(time_t currTime) {
|
||||
if (_stepAI != nullptr)
|
||||
_stepAI(this, currTime);
|
||||
}
|
||||
|
||||
virtual bool isAlive() override { return appearanceData.iHP > 0; }
|
||||
};
|
||||
|
||||
// Mob is in MobAI.hpp, Player is in Player.hpp
|
||||
|
||||
// TODO: decouple from BaseNPC
|
||||
struct Egg : public BaseNPC {
|
||||
bool summoned = false;
|
||||
bool dead = false;
|
||||
time_t deadUntil;
|
||||
|
||||
Egg(int x, int y, int z, uint64_t iID, int t, int32_t id, bool summon)
|
||||
: BaseNPC(x, y, z, 0, iID, t, id) {
|
||||
summoned = summon;
|
||||
type = EntityType::EGG;
|
||||
}
|
||||
|
||||
virtual bool isAlive() override { return !dead; }
|
||||
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
};
|
||||
|
||||
// TODO: decouple from BaseNPC
|
||||
struct Bus : public BaseNPC {
|
||||
Bus(int x, int y, int z, int angle, uint64_t iID, int t, int id) :
|
||||
BaseNPC(x, y, z, angle, iID, t, id) {
|
||||
type = EntityType::BUS;
|
||||
loopingPath = true;
|
||||
}
|
||||
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
};
|
||||
334
src/Groups.cpp
Normal file
334
src/Groups.cpp
Normal file
@@ -0,0 +1,334 @@
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Groups.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "Abilities.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
/*
|
||||
* NOTE: Variadic response packets that list group members are technically
|
||||
* double-variadic, as they have two count members with trailing struct counts,
|
||||
* and are thus incompatible with the generic sendPacket() wrapper.
|
||||
* That means we still have to (carefully) use validOutVarPacket() in this
|
||||
* source file.
|
||||
*/
|
||||
|
||||
using namespace Groups;
|
||||
|
||||
static void requestGroup(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_GROUP_INVITE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_To);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
// fail if the group is full or the other player is already in a group
|
||||
if (plr->groupCnt >= 4 || otherPlr->iIDGroup != otherPlr->iID || otherPlr->groupCnt > 1) {
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_FAIL, resp);
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_FAIL, sizeof(sP_FE2CL_PC_GROUP_INVITE_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(recv->iID_To);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE, resp);
|
||||
|
||||
resp.iHostID = plr->iID;
|
||||
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE, sizeof(sP_FE2CL_PC_GROUP_INVITE));
|
||||
}
|
||||
|
||||
static void refuseGroup(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_GROUP_INVITE_REFUSE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE_REFUSE*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(recv->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_REFUSE, resp);
|
||||
|
||||
resp.iID_To = plr->iID;
|
||||
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_REFUSE, sizeof(sP_FE2CL_PC_GROUP_INVITE_REFUSE));
|
||||
}
|
||||
|
||||
static void joinGroup(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_GROUP_JOIN* recv = (sP_CL2FE_REQ_PC_GROUP_JOIN*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_From);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
// fail if the group is full or the other player is already in a group
|
||||
if (plr->groupCnt > 1 || plr->iIDGroup != plr->iID || otherPlr->groupCnt >= 4) {
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_JOIN_FAIL, resp);
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_JOIN_FAIL, sizeof(sP_FE2CL_PC_GROUP_JOIN_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), otherPlr->groupCnt + 1, sizeof(sPCGroupMemberInfo))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
plr->iIDGroup = otherPlr->iID;
|
||||
otherPlr->groupCnt += 1;
|
||||
otherPlr->groupIDs[otherPlr->groupCnt-1] = plr->iID;
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + otherPlr->groupCnt * sizeof(sPCGroupMemberInfo);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_PC_GROUP_JOIN *resp = (sP_FE2CL_PC_GROUP_JOIN*)respbuf;
|
||||
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_JOIN));
|
||||
|
||||
resp->iID_NewMember = plr->iID;
|
||||
resp->iMemberPCCnt = otherPlr->groupCnt;
|
||||
|
||||
int bitFlag = getGroupFlags(otherPlr);
|
||||
|
||||
for (int i = 0; i < otherPlr->groupCnt; i++) {
|
||||
Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
|
||||
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]);
|
||||
|
||||
if (varPlr == nullptr || sockTo == nullptr)
|
||||
continue;
|
||||
|
||||
respdata[i].iPC_ID = varPlr->iID;
|
||||
respdata[i].iPCUID = varPlr->PCStyle.iPC_UID;
|
||||
respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck;
|
||||
memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
|
||||
memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
|
||||
respdata[i].iSpecialState = varPlr->iSpecialState;
|
||||
respdata[i].iLv = varPlr->level;
|
||||
respdata[i].iHP = varPlr->HP;
|
||||
respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level);
|
||||
//respdata[i].iMapType = 0;
|
||||
//respdata[i].iMapNum = 0;
|
||||
respdata[i].iX = varPlr->x;
|
||||
respdata[i].iY = varPlr->y;
|
||||
respdata[i].iZ = varPlr->z;
|
||||
// client doesnt read nano data here
|
||||
|
||||
if (varPlr != plr) { // apply the new member's buffs to the group and the group's buffs to the new member
|
||||
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
|
||||
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 1, 1, bitFlag);
|
||||
if (Nanos::SkillTable[plr->Nanos[plr->activeNano].iSkillID].targetType == 3)
|
||||
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 1, 1, bitFlag);
|
||||
}
|
||||
}
|
||||
|
||||
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_JOIN, resplen);
|
||||
}
|
||||
|
||||
static void leaveGroup(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
groupKickPlayer(plr);
|
||||
}
|
||||
|
||||
void Groups::sendToGroup(Player* plr, void* buf, uint32_t type, size_t size) {
|
||||
for (int i = 0; i < plr->groupCnt; i++) {
|
||||
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[i]);
|
||||
|
||||
if (sock == nullptr)
|
||||
continue;
|
||||
|
||||
if (type == P_FE2CL_PC_GROUP_LEAVE_SUCC) {
|
||||
Player* leavingPlr = PlayerManager::getPlayer(sock);
|
||||
leavingPlr->iIDGroup = leavingPlr->iID;
|
||||
}
|
||||
|
||||
sock->sendPacket(buf, type, size);
|
||||
}
|
||||
}
|
||||
|
||||
void Groups::groupTickInfo(Player* plr) {
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), plr->groupCnt, sizeof(sPCGroupMemberInfo))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + plr->groupCnt * sizeof(sPCGroupMemberInfo);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_PC_GROUP_MEMBER_INFO *resp = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf;
|
||||
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO));
|
||||
|
||||
resp->iID = plr->iID;
|
||||
resp->iMemberPCCnt = plr->groupCnt;
|
||||
|
||||
for (int i = 0; i < plr->groupCnt; i++) {
|
||||
Player* varPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
|
||||
|
||||
if (varPlr == nullptr)
|
||||
continue;
|
||||
|
||||
respdata[i].iPC_ID = varPlr->iID;
|
||||
respdata[i].iPCUID = varPlr->PCStyle.iPC_UID;
|
||||
respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck;
|
||||
memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
|
||||
memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
|
||||
respdata[i].iSpecialState = varPlr->iSpecialState;
|
||||
respdata[i].iLv = varPlr->level;
|
||||
respdata[i].iHP = varPlr->HP;
|
||||
respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level);
|
||||
//respdata[i].iMapType = 0;
|
||||
//respdata[i].iMapNum = 0;
|
||||
respdata[i].iX = varPlr->x;
|
||||
respdata[i].iY = varPlr->y;
|
||||
respdata[i].iZ = varPlr->z;
|
||||
if (varPlr->activeNano > 0) {
|
||||
respdata[i].bNano = 1;
|
||||
respdata[i].Nano = varPlr->Nanos[varPlr->activeNano];
|
||||
}
|
||||
}
|
||||
|
||||
sendToGroup(plr, (void*)&respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO, resplen);
|
||||
}
|
||||
|
||||
static void groupUnbuff(Player* plr) {
|
||||
for (int i = 0; i < plr->groupCnt; i++) {
|
||||
for (int n = 0; n < plr->groupCnt; n++) {
|
||||
if (i == n)
|
||||
continue;
|
||||
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
|
||||
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[n]);
|
||||
|
||||
Nanos::applyBuff(sock, otherPlr->Nanos[otherPlr->activeNano].iSkillID, 2, 1, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Groups::groupKickPlayer(Player* plr) {
|
||||
// if you are the group leader, destroy your own group and kick everybody
|
||||
if (plr->iID == plr->iIDGroup) {
|
||||
groupUnbuff(plr);
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1);
|
||||
sendToGroup(plr, (void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
|
||||
plr->groupCnt = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), otherPlr->groupCnt - 1, sizeof(sPCGroupMemberInfo))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_LEAVE) + (otherPlr->groupCnt - 1) * sizeof(sPCGroupMemberInfo);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_PC_GROUP_LEAVE *resp = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf;
|
||||
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_LEAVE));
|
||||
|
||||
resp->iID_LeaveMember = plr->iID;
|
||||
resp->iMemberPCCnt = otherPlr->groupCnt - 1;
|
||||
|
||||
int bitFlag = getGroupFlags(otherPlr) & ~plr->iGroupConditionBitFlag;
|
||||
int moveDown = 0;
|
||||
|
||||
CNSocket* sock = PlayerManager::getSockFromID(plr->iID);
|
||||
|
||||
if (sock == nullptr)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < otherPlr->groupCnt; i++) {
|
||||
Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
|
||||
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]);
|
||||
|
||||
if (varPlr == nullptr || sockTo == nullptr)
|
||||
continue;
|
||||
|
||||
if (moveDown == 1)
|
||||
otherPlr->groupIDs[i-1] = otherPlr->groupIDs[i];
|
||||
|
||||
respdata[i-moveDown].iPC_ID = varPlr->iID;
|
||||
respdata[i-moveDown].iPCUID = varPlr->PCStyle.iPC_UID;
|
||||
respdata[i-moveDown].iNameCheck = varPlr->PCStyle.iNameCheck;
|
||||
memcpy(respdata[i-moveDown].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
|
||||
memcpy(respdata[i-moveDown].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
|
||||
respdata[i-moveDown].iSpecialState = varPlr->iSpecialState;
|
||||
respdata[i-moveDown].iLv = varPlr->level;
|
||||
respdata[i-moveDown].iHP = varPlr->HP;
|
||||
respdata[i-moveDown].iMaxHP = PC_MAXHEALTH(varPlr->level);
|
||||
// respdata[i-moveDown]].iMapType = 0;
|
||||
// respdata[i-moveDown]].iMapNum = 0;
|
||||
respdata[i-moveDown].iX = varPlr->x;
|
||||
respdata[i-moveDown].iY = varPlr->y;
|
||||
respdata[i-moveDown].iZ = varPlr->z;
|
||||
// client doesnt read nano data here
|
||||
|
||||
if (varPlr == plr) {
|
||||
moveDown = 1;
|
||||
otherPlr->groupIDs[i] = 0;
|
||||
} else { // remove the leaving member's buffs from the group and remove the group buffs from the leaving member.
|
||||
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
|
||||
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 2, 1, 0);
|
||||
if (Nanos::SkillTable[plr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
|
||||
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 2, 1, bitFlag);
|
||||
}
|
||||
}
|
||||
|
||||
plr->iIDGroup = plr->iID;
|
||||
otherPlr->groupCnt -= 1;
|
||||
|
||||
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_LEAVE, resplen);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1);
|
||||
sock->sendPacket((void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
|
||||
}
|
||||
|
||||
int Groups::getGroupFlags(Player* plr) {
|
||||
int bitFlag = 0;
|
||||
|
||||
for (int i = 0; i < plr->groupCnt; i++) {
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
|
||||
|
||||
if (otherPlr == nullptr)
|
||||
continue;
|
||||
|
||||
bitFlag |= otherPlr->iGroupConditionBitFlag;
|
||||
}
|
||||
|
||||
return bitFlag;
|
||||
}
|
||||
|
||||
void Groups::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_INVITE, requestGroup);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_INVITE_REFUSE, refuseGroup);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_JOIN, joinGroup);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_LEAVE, leaveGroup);
|
||||
}
|
||||
17
src/Groups.hpp
Normal file
17
src/Groups.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "core/Core.hpp"
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <list>
|
||||
|
||||
namespace Groups {
|
||||
void init();
|
||||
|
||||
void sendToGroup(Player* plr, void* buf, uint32_t type, size_t size);
|
||||
void groupTickInfo(Player* plr);
|
||||
void groupKickPlayer(Player* plr);
|
||||
int getGroupFlags(Player* plr);
|
||||
}
|
||||
855
src/Items.cpp
Normal file
855
src/Items.cpp
Normal file
@@ -0,0 +1,855 @@
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
#include "Player.hpp"
|
||||
#include "Abilities.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Eggs.hpp"
|
||||
#include "Rand.hpp"
|
||||
|
||||
#include <string.h> // for memset()
|
||||
#include <assert.h>
|
||||
|
||||
using namespace Items;
|
||||
|
||||
std::map<std::pair<int32_t, int32_t>, Items::Item> Items::ItemData;
|
||||
std::map<int32_t, CrocPotEntry> Items::CrocPotTable;
|
||||
std::map<int32_t, std::vector<int32_t>> Items::RarityWeights;
|
||||
std::map<int32_t, Crate> Items::Crates;
|
||||
std::map<int32_t, ItemReference> Items::ItemReferences;
|
||||
std::map<std::string, std::vector<std::pair<int32_t, int32_t>>> Items::CodeItems;
|
||||
|
||||
std::map<int32_t, CrateDropChance> Items::CrateDropChances;
|
||||
std::map<int32_t, std::vector<int32_t>> Items::CrateDropTypes;
|
||||
std::map<int32_t, MiscDropChance> Items::MiscDropChances;
|
||||
std::map<int32_t, MiscDropType> Items::MiscDropTypes;
|
||||
std::map<int32_t, MobDrop> Items::MobDrops;
|
||||
std::map<int32_t, int32_t> Items::EventToDropMap;
|
||||
std::map<int32_t, int32_t> Items::MobToDropMap;
|
||||
std::map<int32_t, ItemSet> Items::ItemSets;
|
||||
|
||||
#ifdef ACADEMY
|
||||
std::map<int32_t, int32_t> Items::NanoCapsules; // crate id -> nano id
|
||||
|
||||
static void nanoCapsuleHandler(CNSocket* sock, int slot, sItemBase *chest) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
int32_t nanoId = NanoCapsules[chest->iID];
|
||||
|
||||
// chest opening acknowledgement packet
|
||||
INITSTRUCT(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, resp);
|
||||
resp.iSlotNum = slot;
|
||||
|
||||
// in order to remove capsule form inventory, we have to send item reward packet with empty item
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
sP_FE2CL_REP_REWARD_ITEM* reward = (sP_FE2CL_REP_REWARD_ITEM*)respbuf;
|
||||
sItemReward* item = (sItemReward*)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||
|
||||
// don't forget to zero the buffer!
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
// maintain stats
|
||||
reward->m_iCandy = plr->money;
|
||||
reward->m_iFusionMatter = plr->fusionmatter;
|
||||
reward->iFatigue = 100; // prevents warning message
|
||||
reward->iFatigue_Level = 1;
|
||||
reward->iItemCnt = 1; // remember to update resplen if you change this
|
||||
reward->m_iBatteryN = plr->batteryN;
|
||||
reward->m_iBatteryW = plr->batteryW;
|
||||
|
||||
item->iSlotNum = slot;
|
||||
item->eIL = 1;
|
||||
|
||||
// update player serverside
|
||||
plr->Inven[slot] = item->sItem;
|
||||
|
||||
// transmit item
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||
|
||||
// transmit chest opening acknowledgement packet
|
||||
sock->sendPacket(resp, P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC);
|
||||
|
||||
// check if player doesn't already have this nano
|
||||
if (plr->Nanos[nanoId].iID != 0) {
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
|
||||
msg.iDuringTime = 4;
|
||||
std::string text = "You have already acquired this nano!";
|
||||
U8toU16(text, msg.szAnnounceMsg, sizeof(text));
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
return;
|
||||
}
|
||||
Nanos::addNano(sock, nanoId, -1, false);
|
||||
}
|
||||
#endif
|
||||
|
||||
static int choice(const std::vector<int>& weights, int rolled) {
|
||||
int total = std::accumulate(weights.begin(), weights.end(), 0);
|
||||
int randValue = rolled % total;
|
||||
int currentIndex = -1;
|
||||
|
||||
do {
|
||||
currentIndex++;
|
||||
randValue -= weights[currentIndex];
|
||||
} while (randValue >= 0);
|
||||
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
static int getRarity(int crateId, int itemSetId) {
|
||||
Crate& crate = Items::Crates[crateId];
|
||||
|
||||
// find rarity ratio
|
||||
if (Items::RarityWeights.find(crate.rarityWeightId) == Items::RarityWeights.end()) {
|
||||
std::cout << "[WARN] Rarity Weight " << crate.rarityWeightId << " not found!" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<int>& rarityWeights = Items::RarityWeights[crate.rarityWeightId];
|
||||
ItemSet& itemSet = Items::ItemSets[itemSetId];
|
||||
|
||||
/*
|
||||
* First we have to check if specified item set contains items with all specified rarities,
|
||||
* and if not eliminate them from the draw
|
||||
* it is simpler to do here than to fix individually in the file
|
||||
*/
|
||||
|
||||
// remember that rarities start from 1!
|
||||
std::set<int> rarityIndices;
|
||||
|
||||
for (int itemReferenceId : itemSet.itemReferenceIds) {
|
||||
if (Items::ItemReferences.find(itemReferenceId) == Items::ItemReferences.end())
|
||||
continue;
|
||||
|
||||
// alter rarity
|
||||
int itemRarity = (itemSet.alterRarityMap.find(itemReferenceId) == itemSet.alterRarityMap.end())
|
||||
? Items::ItemReferences[itemReferenceId].rarity
|
||||
: itemSet.alterRarityMap[itemReferenceId];
|
||||
|
||||
rarityIndices.insert(itemRarity - 1);
|
||||
|
||||
// shortcut
|
||||
if (rarityIndices.size() == rarityWeights.size())
|
||||
break;
|
||||
}
|
||||
|
||||
if (rarityIndices.empty()) {
|
||||
std::cout << "[WARN] Item Set " << crate.itemSetId << " has no valid items assigned?!" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// retain the weights of rarities that actually exist in the itemset
|
||||
std::vector<int> relevantWeights(rarityWeights.size(), 0);
|
||||
for (int index : rarityIndices) {
|
||||
// check for out of bounds and rarity 0 items
|
||||
if (index >= 0 && index < rarityWeights.size())
|
||||
relevantWeights[index] = rarityWeights[index];
|
||||
}
|
||||
|
||||
// now return a random rarity number (starting from 1)
|
||||
// if relevantWeights is empty or all zeros, we default to giving a common (1) item
|
||||
// rarity 0 items will appear in the drop pool regardless of this roll
|
||||
return Rand::randWeighted(relevantWeights) + 1;
|
||||
}
|
||||
|
||||
static int getCrateItem(sItemBase* result, int itemSetId, int rarity, int playerGender) {
|
||||
ItemSet& itemSet = Items::ItemSets[itemSetId];
|
||||
|
||||
// collect valid items that match the rarity and gender (if not ignored)
|
||||
std::vector<std::pair<int, ItemReference*>> validItems;
|
||||
|
||||
for (int itemReferenceId : itemSet.itemReferenceIds) {
|
||||
if (Items::ItemReferences.find(itemReferenceId) == Items::ItemReferences.end()) {
|
||||
std::cout << "[WARN] Item reference " << itemReferenceId << " in item set type "
|
||||
<< itemSetId << " was not found, skipping..." << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
ItemReference* item = &Items::ItemReferences[itemReferenceId];
|
||||
|
||||
// alter rarity
|
||||
int itemRarity = (itemSet.alterRarityMap.find(itemReferenceId) == itemSet.alterRarityMap.end())
|
||||
? item->rarity
|
||||
: itemSet.alterRarityMap[itemReferenceId];
|
||||
|
||||
// if rarity doesn't match the selected one, exclude item
|
||||
// rarity 0 bypasses this step for an individual item
|
||||
if (!itemSet.ignoreRarity && itemRarity != 0 && itemRarity != rarity)
|
||||
continue;
|
||||
|
||||
// alter rarity
|
||||
int itemGender = (itemSet.alterGenderMap.find(itemReferenceId) == itemSet.alterGenderMap.end())
|
||||
? item->gender
|
||||
: itemSet.alterGenderMap[itemReferenceId];
|
||||
|
||||
// if gender is incorrect, exclude item
|
||||
// gender 0 bypasses this step for an individual item
|
||||
if (!itemSet.ignoreGender && itemGender != 0 && itemGender != playerGender)
|
||||
continue;
|
||||
|
||||
validItems.push_back(std::make_pair(itemReferenceId, item));
|
||||
}
|
||||
|
||||
if (validItems.empty()) {
|
||||
std::cout << "[WARN] Set ID " << itemSetId << " Rarity " << rarity << " contains no valid items" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// initialize all weights as the default weight for all item slots
|
||||
std::vector<int> itemWeights(validItems.size(), itemSet.defaultItemWeight);
|
||||
|
||||
if (!itemSet.alterItemWeightMap.empty()) {
|
||||
for (int i = 0; i < validItems.size(); i++) {
|
||||
int itemReferenceId = validItems[i].first;
|
||||
|
||||
if (itemSet.alterItemWeightMap.find(itemReferenceId) == itemSet.alterItemWeightMap.end())
|
||||
continue;
|
||||
|
||||
int weight = itemSet.alterItemWeightMap[itemReferenceId];
|
||||
// allow 0 weights for convenience
|
||||
if (weight > -1)
|
||||
itemWeights[i] = weight;
|
||||
}
|
||||
}
|
||||
|
||||
int chosenIndex = Rand::randWeighted(itemWeights);
|
||||
ItemReference* item = validItems[chosenIndex].second;
|
||||
|
||||
result->iID = item->itemId;
|
||||
result->iType = item->type;
|
||||
result->iOpt = 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int getValidCrateId(int crateId) {
|
||||
// find the crate
|
||||
if (Items::Crates.find(crateId) == Items::Crates.end()) {
|
||||
std::cout << "[WARN] Crate " << crateId << " not found!" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return crateId;
|
||||
}
|
||||
|
||||
static int getValidItemSetId(int crateId) {
|
||||
Crate& crate = Items::Crates[crateId];
|
||||
|
||||
// find item set type
|
||||
if (Items::ItemSets.find(crate.itemSetId) == Items::ItemSets.end()) {
|
||||
std::cout << "[WARN] Crate " << crateId << " was assigned item set "
|
||||
<< crate.itemSetId << " which is invalid!" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return crate.itemSetId;
|
||||
}
|
||||
|
||||
static void itemMoveHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto itemmove = (sP_CL2FE_REQ_ITEM_MOVE*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_PC_ITEM_MOVE_SUCC, resp);
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// sanity check
|
||||
if (itemmove->iToSlotNum < 0 || itemmove->iFromSlotNum < 0)
|
||||
return;
|
||||
// NOTE: sending a no-op, "move in-place" packet is not necessary
|
||||
|
||||
if (plr->isTrading) {
|
||||
std::cout << "[WARN] Player attempted to move item while trading" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// get the fromItem
|
||||
sItemBase *fromItem;
|
||||
switch ((SlotType)itemmove->eFrom) {
|
||||
case SlotType::EQUIP:
|
||||
if (itemmove->iFromSlotNum >= AEQUIP_COUNT)
|
||||
return;
|
||||
|
||||
fromItem = &plr->Equip[itemmove->iFromSlotNum];
|
||||
break;
|
||||
case SlotType::INVENTORY:
|
||||
if (itemmove->iFromSlotNum >= AINVEN_COUNT)
|
||||
return;
|
||||
|
||||
fromItem = &plr->Inven[itemmove->iFromSlotNum];
|
||||
break;
|
||||
case SlotType::BANK:
|
||||
if (itemmove->iFromSlotNum >= ABANK_COUNT)
|
||||
return;
|
||||
|
||||
fromItem = &plr->Bank[itemmove->iFromSlotNum];
|
||||
break;
|
||||
default:
|
||||
std::cout << "[WARN] MoveItem submitted unknown Item Type?! " << itemmove->eFrom << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// get the toItem
|
||||
sItemBase* toItem;
|
||||
switch ((SlotType)itemmove->eTo) {
|
||||
case SlotType::EQUIP:
|
||||
if (itemmove->iToSlotNum >= AEQUIP_COUNT)
|
||||
return;
|
||||
|
||||
toItem = &plr->Equip[itemmove->iToSlotNum];
|
||||
break;
|
||||
case SlotType::INVENTORY:
|
||||
if (itemmove->iToSlotNum >= AINVEN_COUNT)
|
||||
return;
|
||||
|
||||
toItem = &plr->Inven[itemmove->iToSlotNum];
|
||||
break;
|
||||
case SlotType::BANK:
|
||||
if (itemmove->iToSlotNum >= ABANK_COUNT)
|
||||
return;
|
||||
|
||||
toItem = &plr->Bank[itemmove->iToSlotNum];
|
||||
break;
|
||||
default:
|
||||
std::cout << "[WARN] MoveItem submitted unknown Item Type?! " << itemmove->eTo << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// if equipping an item, validate that it's of the correct type for the slot
|
||||
if ((SlotType)itemmove->eTo == SlotType::EQUIP) {
|
||||
if (fromItem->iType == 10 && itemmove->iToSlotNum != 8)
|
||||
return; // vehicle in wrong slot
|
||||
else if (fromItem->iType != 10
|
||||
&& !(fromItem->iType == 0 && itemmove->iToSlotNum == 7)
|
||||
&& fromItem->iType != itemmove->iToSlotNum)
|
||||
return; // something other than a vehicle or a weapon in a non-matching slot
|
||||
else if (itemmove->iToSlotNum >= AEQUIP_COUNT) // TODO: reject slots >= 9?
|
||||
return; // invalid slot
|
||||
}
|
||||
|
||||
// save items to response
|
||||
resp.eTo = itemmove->eFrom;
|
||||
resp.eFrom = itemmove->eTo;
|
||||
resp.ToSlotItem = *toItem;
|
||||
resp.FromSlotItem = *fromItem;
|
||||
|
||||
// swap/stack items in session
|
||||
Item* itemDat = getItemData(toItem->iID, toItem->iType);
|
||||
Item* itemDatFrom = getItemData(fromItem->iID, fromItem->iType);
|
||||
if (itemDat != nullptr && itemDatFrom != nullptr && itemDat->stackSize > 1 && itemDat == itemDatFrom && fromItem->iOpt < itemDat->stackSize && toItem->iOpt < itemDat->stackSize) {
|
||||
// items are stackable, identical, and not maxed, so run stacking logic
|
||||
|
||||
toItem->iOpt += fromItem->iOpt; // sum counts
|
||||
fromItem->iOpt = 0; // deplete from item
|
||||
if (toItem->iOpt > itemDat->stackSize) {
|
||||
// handle overflow
|
||||
fromItem->iOpt += (toItem->iOpt - itemDat->stackSize); // add overflow to fromItem
|
||||
toItem->iOpt = itemDat->stackSize; // set toItem count to max
|
||||
}
|
||||
|
||||
if (fromItem->iOpt == 0) { // from item count depleted
|
||||
// delete item
|
||||
fromItem->iID = 0;
|
||||
fromItem->iType = 0;
|
||||
fromItem->iTimeLimit = 0;
|
||||
}
|
||||
|
||||
resp.iFromSlotNum = itemmove->iFromSlotNum;
|
||||
resp.iToSlotNum = itemmove->iToSlotNum;
|
||||
resp.FromSlotItem = *fromItem;
|
||||
resp.ToSlotItem = *toItem;
|
||||
} else {
|
||||
// items not stackable; just swap them
|
||||
sItemBase temp = *toItem;
|
||||
*toItem = *fromItem;
|
||||
*fromItem = temp;
|
||||
resp.iFromSlotNum = itemmove->iToSlotNum;
|
||||
resp.iToSlotNum = itemmove->iFromSlotNum;
|
||||
}
|
||||
|
||||
// send equip change to viewable players
|
||||
if (itemmove->eFrom == (int)SlotType::EQUIP || itemmove->eTo == (int)SlotType::EQUIP) {
|
||||
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, equipChange);
|
||||
|
||||
equipChange.iPC_ID = plr->iID;
|
||||
if (itemmove->eTo == (int)SlotType::EQUIP) {
|
||||
equipChange.iEquipSlotNum = itemmove->iToSlotNum;
|
||||
equipChange.EquipSlotItem = resp.FromSlotItem;
|
||||
} else {
|
||||
equipChange.iEquipSlotNum = itemmove->iFromSlotNum;
|
||||
equipChange.EquipSlotItem = resp.ToSlotItem;
|
||||
}
|
||||
|
||||
// unequip vehicle if equip slot 8 is 0
|
||||
if (plr->Equip[8].iID == 0 && plr->iPCState & 8) {
|
||||
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response);
|
||||
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC);
|
||||
|
||||
// send to other players
|
||||
plr->iPCState &= ~8;
|
||||
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, response2);
|
||||
response2.iPC_ID = plr->iID;
|
||||
response2.iState = plr->iPCState;
|
||||
|
||||
PlayerManager::sendToViewable(sock, response2, P_FE2CL_PC_STATE_CHANGE);
|
||||
}
|
||||
|
||||
// send equip event to other players
|
||||
PlayerManager::sendToViewable(sock, equipChange, P_FE2CL_PC_EQUIP_CHANGE);
|
||||
|
||||
// set equipment stats serverside
|
||||
setItemStats(plr);
|
||||
}
|
||||
|
||||
// send response
|
||||
sock->sendPacket(resp, P_FE2CL_PC_ITEM_MOVE_SUCC);
|
||||
}
|
||||
|
||||
static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto itemdel = (sP_CL2FE_REQ_PC_ITEM_DELETE*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_DELETE_SUCC, resp);
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
resp.eIL = itemdel->eIL;
|
||||
resp.iSlotNum = itemdel->iSlotNum;
|
||||
|
||||
// so, im not sure what this eIL thing does since you always delete items in inventory and not equips
|
||||
plr->Inven[itemdel->iSlotNum].iID = 0;
|
||||
plr->Inven[itemdel->iSlotNum].iType = 0;
|
||||
plr->Inven[itemdel->iSlotNum].iOpt = 0;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_DELETE_SUCC);
|
||||
}
|
||||
|
||||
static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf;
|
||||
Player* player = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (request->iSlotNum < 0 || request->iSlotNum >= AINVEN_COUNT)
|
||||
return; // sanity check
|
||||
|
||||
// gumball can only be used from inventory, so we ignore eIL
|
||||
sItemBase gumball = player->Inven[request->iSlotNum];
|
||||
sNano nano = player->Nanos[player->equippedNanos[request->iNanoSlot]];
|
||||
|
||||
// sanity check, check if gumball exists
|
||||
if (!(gumball.iOpt > 0 && gumball.iType == 7 && gumball.iID>=119 && gumball.iID<=121)) {
|
||||
std::cout << "[WARN] Gumball not found" << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
|
||||
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// sanity check, check if gumball type matches nano style
|
||||
int nanoStyle = Nanos::nanoStyle(nano.iID);
|
||||
if (!((gumball.iID == 119 && nanoStyle == 0) ||
|
||||
( gumball.iID == 120 && nanoStyle == 1) ||
|
||||
( gumball.iID == 121 && nanoStyle == 2))) {
|
||||
std::cout << "[WARN] Gumball type doesn't match nano type" << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
|
||||
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
gumball.iOpt -= 1;
|
||||
if (gumball.iOpt == 0)
|
||||
gumball = {};
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_REP_PC_ITEM_USE_SUCC) + sizeof(sSkillResult_Buff);
|
||||
|
||||
// validate response packet
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_REP_PC_ITEM_USE_SUCC), 1, sizeof(sSkillResult_Buff))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_REP_PC_ITEM_USE_SUCC packet size" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (gumball.iOpt == 0)
|
||||
gumball = {};
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_REP_PC_ITEM_USE_SUCC *resp = (sP_FE2CL_REP_PC_ITEM_USE_SUCC*)respbuf;
|
||||
sSkillResult_Buff *respdata = (sSkillResult_Buff*)(respbuf+sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC));
|
||||
resp->iPC_ID = player->iID;
|
||||
resp->eIL = 1;
|
||||
resp->iSlotNum = request->iSlotNum;
|
||||
resp->RemainItem = gumball;
|
||||
resp->iTargetCnt = 1;
|
||||
resp->eST = EST_NANOSTIMPAK;
|
||||
resp->iSkillID = 144;
|
||||
|
||||
int value1 = CSB_BIT_STIMPAKSLOT1 << request->iNanoSlot;
|
||||
int value2 = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
|
||||
|
||||
respdata->eCT = 1;
|
||||
respdata->iID = player->iID;
|
||||
respdata->iConditionBitFlag = value1;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
|
||||
pkt.eCSTB = value2; // eCharStatusTimeBuffID
|
||||
pkt.eTBU = 1; // eTimeBuffUpdate
|
||||
pkt.eTBT = 1; // eTimeBuffType 1 means nano
|
||||
pkt.iConditionBitFlag = player->iConditionBitFlag |= value1;
|
||||
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
|
||||
|
||||
sock->sendPacket((void*)&respbuf, P_FE2CL_REP_PC_ITEM_USE_SUCC, resplen);
|
||||
// update inventory serverside
|
||||
player->Inven[resp->iSlotNum] = resp->RemainItem;
|
||||
|
||||
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, value1);
|
||||
time_t until = getTime() + (time_t)Nanos::SkillTable[144].durationTime[0] * 100;
|
||||
Eggs::EggBuffs[key] = until;
|
||||
}
|
||||
|
||||
static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// just send bank inventory
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_BANK_OPEN_SUCC, resp);
|
||||
for (int i = 0; i < ABANK_COUNT; i++) {
|
||||
resp.aBank[i] = plr->Bank[i];
|
||||
}
|
||||
resp.iExtraBank = 1;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_BANK_OPEN_SUCC);
|
||||
}
|
||||
|
||||
static void chestOpenHandler(CNSocket *sock, CNPacketData *data) {
|
||||
auto pkt = (sP_CL2FE_REQ_ITEM_CHEST_OPEN *)data->buf;
|
||||
|
||||
// sanity check
|
||||
if (pkt->eIL != 1 || pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT)
|
||||
return;
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
sItemBase *chest = &plr->Inven[pkt->iSlotNum];
|
||||
// we could reject the packet if the client thinks the item is different, but eh
|
||||
|
||||
if (chest->iType != 9) {
|
||||
std::cout << "[WARN] Player tried to open a crate with incorrect iType ?!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ACADEMY
|
||||
// check if chest isn't a nano capsule
|
||||
if (NanoCapsules.find(chest->iID) != NanoCapsules.end())
|
||||
return nanoCapsuleHandler(sock, pkt->iSlotNum, chest);
|
||||
#endif
|
||||
|
||||
// chest opening acknowledgement packet
|
||||
INITSTRUCT(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, resp);
|
||||
resp.iSlotNum = pkt->iSlotNum;
|
||||
|
||||
// item giving packet
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
sP_FE2CL_REP_REWARD_ITEM *reward = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
|
||||
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||
|
||||
// don't forget to zero the buffer!
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
// maintain stats
|
||||
reward->m_iCandy = plr->money;
|
||||
reward->m_iFusionMatter = plr->fusionmatter;
|
||||
reward->iFatigue = 100; // prevents warning message
|
||||
reward->iFatigue_Level = 1;
|
||||
reward->iItemCnt = 1; // remember to update resplen if you change this
|
||||
reward->m_iBatteryN = plr->batteryN;
|
||||
reward->m_iBatteryW = plr->batteryW;
|
||||
|
||||
item->iSlotNum = pkt->iSlotNum;
|
||||
item->eIL = 1;
|
||||
|
||||
int validItemSetId = -1, rarity = -1, ret = -1;
|
||||
|
||||
int validCrateId = getValidCrateId(chest->iID);
|
||||
bool failing = (validCrateId == -1);
|
||||
|
||||
if (!failing)
|
||||
validItemSetId = getValidItemSetId(validCrateId);
|
||||
failing = (validItemSetId == -1);
|
||||
|
||||
if (!failing)
|
||||
rarity = getRarity(validCrateId, validItemSetId);
|
||||
failing = (rarity == -1);
|
||||
|
||||
if (!failing)
|
||||
ret = getCrateItem(&item->sItem, validItemSetId, rarity, plr->PCStyle.iGender);
|
||||
failing = (ret == -1);
|
||||
|
||||
// if we failed to open a crate, at least give the player a gumball (suggested by Jade)
|
||||
if (failing) {
|
||||
item->sItem.iType = 7;
|
||||
item->sItem.iID = 119 + Rand::rand(3);
|
||||
item->sItem.iOpt = 1;
|
||||
|
||||
std::cout << "[WARN] Crate open failed, giving a Gumball..." << std::endl;
|
||||
}
|
||||
// update player
|
||||
plr->Inven[pkt->iSlotNum] = item->sItem;
|
||||
|
||||
// transmit item
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||
|
||||
// transmit chest opening acknowledgement packet
|
||||
std::cout << "opening chest..." << std::endl;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC);
|
||||
}
|
||||
|
||||
// TODO: use this in cleaned up Items
|
||||
int Items::findFreeSlot(Player *plr) {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < AINVEN_COUNT; i++)
|
||||
if (plr->Inven[i].iType == 0 && plr->Inven[i].iID == 0 && plr->Inven[i].iOpt == 0)
|
||||
return i;
|
||||
|
||||
// not found
|
||||
return -1;
|
||||
}
|
||||
|
||||
Item* Items::getItemData(int32_t id, int32_t type) {
|
||||
if(ItemData.find(std::make_pair(id, type)) != ItemData.end())
|
||||
return &ItemData[std::make_pair(id, type)];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Items::checkItemExpire(CNSocket* sock, Player* player) {
|
||||
if (player->toRemoveVehicle.eIL == 0 && player->toRemoveVehicle.iSlotNum == 0)
|
||||
return;
|
||||
|
||||
/* prepare packet
|
||||
* yes, this is a varadic packet, however analyzing client behavior and code
|
||||
* it only checks takes the first item sent into account
|
||||
* yes, this is very stupid
|
||||
* therefore, we delete all but 1 expired vehicle while loading player
|
||||
* to delete the last one here so player gets a notification
|
||||
*/
|
||||
|
||||
const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
auto packet = (sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM*)respbuf;
|
||||
sTimeLimitItemDeleteInfo2CL* itemData = (sTimeLimitItemDeleteInfo2CL*)(respbuf + sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM));
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
packet->iItemListCount = 1;
|
||||
itemData->eIL = player->toRemoveVehicle.eIL;
|
||||
itemData->iSlotNum = player->toRemoveVehicle.iSlotNum;
|
||||
sock->sendPacket((void*)&respbuf, P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, resplen);
|
||||
|
||||
// delete serverside
|
||||
if (player->toRemoveVehicle.eIL == 0)
|
||||
memset(&player->Equip[8], 0, sizeof(sItemBase));
|
||||
else
|
||||
memset(&player->Inven[player->toRemoveVehicle.iSlotNum], 0, sizeof(sItemBase));
|
||||
|
||||
player->toRemoveVehicle.eIL = 0;
|
||||
player->toRemoveVehicle.iSlotNum = 0;
|
||||
}
|
||||
|
||||
void Items::setItemStats(Player* plr) {
|
||||
|
||||
plr->pointDamage = 8 + plr->level * 2;
|
||||
plr->groupDamage = 8 + plr->level * 2;
|
||||
plr->fireRate = 0;
|
||||
plr->defense = 16 + plr->level * 4;
|
||||
|
||||
Item* itemStatsDat;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
itemStatsDat = getItemData(plr->Equip[i].iID, plr->Equip[i].iType);
|
||||
if (itemStatsDat == nullptr) {
|
||||
std::cout << "[WARN] setItemStats(): getItemData() returned NULL" << std::endl;
|
||||
continue;
|
||||
}
|
||||
plr->pointDamage += itemStatsDat->pointDamage;
|
||||
plr->groupDamage += itemStatsDat->groupDamage;
|
||||
plr->fireRate += itemStatsDat->fireRate;
|
||||
plr->defense += itemStatsDat->defense;
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: work around the invisible weapon bug
|
||||
// TODO: I don't think this makes a difference at all? Check and remove, if necessary.
|
||||
void Items::updateEquips(CNSocket* sock, Player* plr) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, resp);
|
||||
|
||||
resp.iPC_ID = plr->iID;
|
||||
resp.iEquipSlotNum = i;
|
||||
resp.EquipSlotItem = plr->Equip[i];
|
||||
|
||||
PlayerManager::sendToViewable(sock, resp, P_FE2CL_PC_EQUIP_CHANGE);
|
||||
}
|
||||
}
|
||||
|
||||
static void getMobDrop(sItemBase* reward, const std::vector<int>& weights, const std::vector<int>& crateIds, int rolled) {
|
||||
int chosenIndex = choice(weights, rolled);
|
||||
|
||||
reward->iType = 9;
|
||||
reward->iOpt = 1;
|
||||
reward->iID = crateIds[chosenIndex];
|
||||
}
|
||||
|
||||
static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRoll& rolled) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
sP_FE2CL_REP_REWARD_ITEM *reward = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
|
||||
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||
|
||||
// don't forget to zero the buffer!
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
// sanity check
|
||||
if (Items::MobDrops.find(mobDropId) == Items::MobDrops.end()) {
|
||||
std::cout << "[WARN] Drop Type " << mobDropId << " was not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
// find correct mob drop
|
||||
MobDrop& drop = Items::MobDrops[mobDropId];
|
||||
|
||||
// use the keys to fetch data from other maps
|
||||
// sanity check
|
||||
if (Items::CrateDropChances.find(drop.crateDropChanceId) == Items::CrateDropChances.end()) {
|
||||
std::cout << "[WARN] Crate Drop Chance Object " << drop.crateDropChanceId << " was not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
CrateDropChance& crateDropChance = Items::CrateDropChances[drop.crateDropChanceId];
|
||||
|
||||
// sanity check
|
||||
if (Items::CrateDropTypes.find(drop.crateDropTypeId) == Items::CrateDropTypes.end()) {
|
||||
std::cout << "[WARN] Crate Drop Type Object " << drop.crateDropTypeId << " was not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
std::vector<int>& crateDropType = Items::CrateDropTypes[drop.crateDropTypeId];
|
||||
|
||||
// sanity check
|
||||
if (Items::MiscDropChances.find(drop.miscDropChanceId) == Items::MiscDropChances.end()) {
|
||||
std::cout << "[WARN] Misc Drop Chance Object " << drop.miscDropChanceId << " was not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
MiscDropChance& miscDropChance = Items::MiscDropChances[drop.miscDropChanceId];
|
||||
|
||||
// sanity check
|
||||
if (Items::MiscDropTypes.find(drop.miscDropTypeId) == Items::MiscDropTypes.end()) {
|
||||
std::cout << "[WARN] Misc Drop Type Object " << drop.miscDropTypeId << " was not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
MiscDropType& miscDropType = Items::MiscDropTypes[drop.miscDropTypeId];
|
||||
|
||||
if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) {
|
||||
plr->money += miscDropType.taroAmount;
|
||||
// money nano boost
|
||||
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) {
|
||||
int boost = 0;
|
||||
if (Nanos::getNanoBoost(plr)) // for gumballs
|
||||
boost = 1;
|
||||
plr->money += miscDropType.taroAmount * (5 + boost) / 25;
|
||||
}
|
||||
}
|
||||
if (rolled.fm % miscDropChance.fmDropChanceTotal < miscDropChance.fmDropChance) {
|
||||
// formula for scaling FM with player/mob level difference
|
||||
// TODO: adjust this better
|
||||
int levelDifference = plr->level - mob->level;
|
||||
int fm = miscDropType.fmAmount;
|
||||
if (levelDifference > 0)
|
||||
fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0;
|
||||
// scavenger nano boost
|
||||
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) {
|
||||
int boost = 0;
|
||||
if (Nanos::getNanoBoost(plr)) // for gumballs
|
||||
boost = 1;
|
||||
fm += fm * (5 + boost) / 25;
|
||||
}
|
||||
|
||||
Missions::updateFusionMatter(sock, fm);
|
||||
}
|
||||
|
||||
if (rolled.potions % miscDropChance.potionDropChanceTotal < miscDropChance.potionDropChance)
|
||||
plr->batteryN += miscDropType.potionAmount;
|
||||
if (rolled.boosts % miscDropChance.boostDropChanceTotal < miscDropChance.boostDropChance)
|
||||
plr->batteryW += miscDropType.boostAmount;
|
||||
|
||||
// caps
|
||||
if (plr->batteryW > 9999)
|
||||
plr->batteryW = 9999;
|
||||
if (plr->batteryN > 9999)
|
||||
plr->batteryN = 9999;
|
||||
|
||||
// simple rewards
|
||||
reward->m_iCandy = plr->money;
|
||||
reward->m_iFusionMatter = plr->fusionmatter;
|
||||
reward->m_iBatteryN = plr->batteryN;
|
||||
reward->m_iBatteryW = plr->batteryW;
|
||||
reward->iFatigue = 100; // prevents warning message
|
||||
reward->iFatigue_Level = 1;
|
||||
reward->iItemCnt = 1; // remember to update resplen if you change this
|
||||
|
||||
int slot = findFreeSlot(plr);
|
||||
|
||||
// no drop
|
||||
if (slot == -1 || rolled.crate % crateDropChance.dropChanceTotal >= crateDropChance.dropChance) {
|
||||
// no room for an item, but you still get FM and taros
|
||||
reward->iItemCnt = 0;
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||
} else {
|
||||
// item reward
|
||||
getMobDrop(&item->sItem, crateDropChance.crateTypeDropWeights, crateDropType, rolled.crateType);
|
||||
item->iSlotNum = slot;
|
||||
item->eIL = 1; // Inventory Location. 1 means player inventory.
|
||||
|
||||
// update player
|
||||
plr->Inven[slot] = item->sItem;
|
||||
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||
}
|
||||
}
|
||||
|
||||
void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled) {
|
||||
// sanity check
|
||||
if (Items::MobToDropMap.find(mob->appearanceData.iNPCType) == Items::MobToDropMap.end()) {
|
||||
std::cout << "[WARN] Mob ID " << mob->appearanceData.iNPCType << " has no drops assigned" << std::endl;
|
||||
return;
|
||||
}
|
||||
// find mob drop id
|
||||
int mobDropId = Items::MobToDropMap[mob->appearanceData.iNPCType];
|
||||
|
||||
giveSingleDrop(sock, mob, mobDropId, rolled);
|
||||
|
||||
if (settings::EVENTMODE != 0) {
|
||||
// sanity check
|
||||
if (Items::EventToDropMap.find(settings::EVENTMODE) == Items::EventToDropMap.end()) {
|
||||
std::cout << "[WARN] Event " << settings::EVENTMODE << " has no mob drop assigned" << std::endl;
|
||||
return;
|
||||
}
|
||||
// find mob drop id
|
||||
int eventMobDropId = Items::EventToDropMap[settings::EVENTMODE];
|
||||
|
||||
giveSingleDrop(sock, mob, eventMobDropId, eventRolled);
|
||||
}
|
||||
}
|
||||
|
||||
void Items::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler);
|
||||
// this one is for gumballs
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_USE, itemUseHandler);
|
||||
// Bank
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BANK_OPEN, itemBankOpenHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_CHEST_OPEN, chestOpenHandler);
|
||||
}
|
||||
123
src/Items.hpp
Normal file
123
src/Items.hpp
Normal file
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "Player.hpp"
|
||||
#include "MobAI.hpp"
|
||||
#include "Rand.hpp"
|
||||
|
||||
struct CrocPotEntry {
|
||||
int multStats, multLooks;
|
||||
float base, rd0, rd1, rd2, rd3;
|
||||
};
|
||||
|
||||
struct Crate {
|
||||
int itemSetId;
|
||||
int rarityWeightId;
|
||||
};
|
||||
|
||||
struct CrateDropChance {
|
||||
int dropChance, dropChanceTotal;
|
||||
std::vector<int> crateTypeDropWeights;
|
||||
};
|
||||
|
||||
struct MiscDropChance {
|
||||
int potionDropChance, potionDropChanceTotal;
|
||||
int boostDropChance, boostDropChanceTotal;
|
||||
int taroDropChance, taroDropChanceTotal;
|
||||
int fmDropChance, fmDropChanceTotal;
|
||||
};
|
||||
|
||||
struct MiscDropType {
|
||||
int potionAmount;
|
||||
int boostAmount;
|
||||
int taroAmount;
|
||||
int fmAmount;
|
||||
};
|
||||
|
||||
struct MobDrop {
|
||||
int crateDropChanceId;
|
||||
int crateDropTypeId;
|
||||
int miscDropChanceId;
|
||||
int miscDropTypeId;
|
||||
};
|
||||
|
||||
struct ItemSet {
|
||||
// itemset-wise offswitch to rarity filtering, every crate drops every rarity (still based on rarity weights)
|
||||
bool ignoreRarity;
|
||||
// itemset-wise offswitch for gender filtering, every crate can now drop neutral/boys/girls items
|
||||
bool ignoreGender;
|
||||
// default weight of all items in the itemset
|
||||
int defaultItemWeight;
|
||||
// change the rarity class of items in the itemset here
|
||||
// rarity 0 bypasses the rarity filter for an individual item
|
||||
std::map<int, int> alterRarityMap;
|
||||
// change the gender class of items in the itemset here
|
||||
// gender 0 bypasses the gender filter for an individual item
|
||||
std::map<int, int> alterGenderMap;
|
||||
// change the item weghts items in the itemset here
|
||||
// only taken into account for chosen rarity, and if the item isn't filtered away due to gender
|
||||
std::map<int, int> alterItemWeightMap;
|
||||
std::vector<int> itemReferenceIds;
|
||||
};
|
||||
|
||||
struct ItemReference {
|
||||
int itemId;
|
||||
int type;
|
||||
int rarity;
|
||||
int gender;
|
||||
};
|
||||
|
||||
namespace Items {
|
||||
enum class SlotType {
|
||||
EQUIP = 0,
|
||||
INVENTORY = 1,
|
||||
BANK = 3
|
||||
};
|
||||
struct Item {
|
||||
bool tradeable, sellable;
|
||||
int buyPrice, sellPrice;
|
||||
int stackSize, level, rarity;
|
||||
int pointDamage, groupDamage, fireRate, defense, gender;
|
||||
int weaponType;
|
||||
// TODO: implement more as needed
|
||||
};
|
||||
struct DropRoll {
|
||||
int boosts, potions;
|
||||
int taros, fm;
|
||||
int crate, crateType;
|
||||
|
||||
DropRoll() : boosts(Rand::rand()), potions(Rand::rand()), taros(Rand::rand()), fm(Rand::rand()), crate(Rand::rand()), crateType(Rand::rand()) { }
|
||||
};
|
||||
// hopefully this is fine since it's never modified after load
|
||||
extern std::map<std::pair<int32_t, int32_t>, Item> ItemData; // <id, type> -> data
|
||||
extern std::map<int32_t, CrocPotEntry> CrocPotTable; // level gap -> entry
|
||||
extern std::map<int32_t, std::vector<int32_t>> RarityWeights;
|
||||
extern std::map<int32_t, Crate> Crates;
|
||||
extern std::map<int32_t, ItemReference> ItemReferences;
|
||||
extern std::map<std::string, std::vector<std::pair<int32_t, int32_t>>> CodeItems; // code -> vector of <id, type>
|
||||
|
||||
// mob drops
|
||||
extern std::map<int32_t, CrateDropChance> CrateDropChances;
|
||||
extern std::map<int32_t, std::vector<int32_t>> CrateDropTypes;
|
||||
extern std::map<int32_t, MiscDropChance> MiscDropChances;
|
||||
extern std::map<int32_t, MiscDropType> MiscDropTypes;
|
||||
extern std::map<int32_t, MobDrop> MobDrops;
|
||||
extern std::map<int32_t, int32_t> EventToDropMap;
|
||||
extern std::map<int32_t, int32_t> MobToDropMap;
|
||||
extern std::map<int32_t, ItemSet> ItemSets;
|
||||
|
||||
void init();
|
||||
|
||||
// mob drops
|
||||
void giveMobDrop(CNSocket *sock, Mob *mob, const DropRoll& rolled, const DropRoll& eventRolled);
|
||||
|
||||
int findFreeSlot(Player *plr);
|
||||
Item* getItemData(int32_t id, int32_t type);
|
||||
void checkItemExpire(CNSocket* sock, Player* player);
|
||||
void setItemStats(Player* plr);
|
||||
void updateEquips(CNSocket* sock, Player* plr);
|
||||
|
||||
#ifdef ACADEMY
|
||||
extern std::map<int32_t, int32_t> NanoCapsules; // crate id -> nano id
|
||||
#endif
|
||||
}
|
||||
659
src/Missions.cpp
Normal file
659
src/Missions.cpp
Normal file
@@ -0,0 +1,659 @@
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "Transport.hpp"
|
||||
|
||||
#include "string.h"
|
||||
|
||||
using namespace Missions;
|
||||
|
||||
std::map<int32_t, Reward*> Missions::Rewards;
|
||||
std::map<int32_t, TaskData*> Missions::Tasks;
|
||||
nlohmann::json Missions::AvatarGrowth[37];
|
||||
|
||||
static void saveMission(Player* player, int missionId) {
|
||||
// sanity check missionID so we don't get exceptions
|
||||
if (missionId < 0 || missionId > 1023) {
|
||||
std::cout << "[WARN] Client submitted invalid missionId: " <<missionId<< std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Missions are stored in int64_t array
|
||||
int row = missionId / 64;
|
||||
int column = missionId % 64;
|
||||
player->aQuestFlag[row] |= (1ULL << column);
|
||||
}
|
||||
|
||||
static bool isMissionCompleted(Player* player, int missionId) {
|
||||
int row = missionId / 64;
|
||||
int column = missionId % 64;
|
||||
return player->aQuestFlag[row] & (1ULL << column);
|
||||
}
|
||||
|
||||
int Missions::findQSlot(Player *plr, int id) {
|
||||
int i;
|
||||
|
||||
// two passes. we mustn't fail to find an existing stack.
|
||||
for (i = 0; i < AQINVEN_COUNT; i++)
|
||||
if (plr->QInven[i].iID == id)
|
||||
return i;
|
||||
|
||||
// no stack. start a new one.
|
||||
for (i = 0; i < AQINVEN_COUNT; i++)
|
||||
if (plr->QInven[i].iOpt == 0)
|
||||
return i;
|
||||
|
||||
// not found
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool isQuestItemFull(CNSocket* sock, int itemId, int itemCount) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int slot = Missions::findQSlot(plr, itemId);
|
||||
if (slot == -1) {
|
||||
// this should never happen
|
||||
std::cout << "[WARN] Player has no room for quest item!?" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
return (itemCount == plr->QInven[slot].iOpt);
|
||||
}
|
||||
|
||||
static void dropQuestItem(CNSocket *sock, int task, int count, int id, int mobid) {
|
||||
std::cout << "Altered item id " << id << " by " << count << " for task id " << task << std::endl;
|
||||
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE);
|
||||
// we know it's only one trailing struct, so we can skip full validation
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
||||
sP_FE2CL_REP_REWARD_ITEM *reward = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
|
||||
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||
|
||||
// don't forget to zero the buffer!
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
// find free quest item slot
|
||||
int slot = Missions::findQSlot(plr, id);
|
||||
if (slot == -1) {
|
||||
// this should never happen
|
||||
std::cout << "[WARN] Player has no room for quest item!?" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (id != 0)
|
||||
std::cout << "new qitem in slot " << slot << std::endl;
|
||||
|
||||
// update player
|
||||
if (id != 0) {
|
||||
plr->QInven[slot].iType = 8;
|
||||
plr->QInven[slot].iID = id;
|
||||
plr->QInven[slot].iOpt += count; // stacking
|
||||
}
|
||||
|
||||
// fully destory deleted items, for good measure
|
||||
if (plr->QInven[slot].iOpt <= 0)
|
||||
memset(&plr->QInven[slot], 0, sizeof(sItemBase));
|
||||
|
||||
// preserve stats
|
||||
reward->m_iCandy = plr->money;
|
||||
reward->m_iFusionMatter = plr->fusionmatter;
|
||||
reward->iFatigue = 100; // prevents warning message
|
||||
reward->iFatigue_Level = 1;
|
||||
reward->m_iBatteryN = plr->batteryN;
|
||||
reward->m_iBatteryW = plr->batteryW;
|
||||
|
||||
reward->iItemCnt = 1; // remember to update resplen if you change this
|
||||
reward->iTaskID = task;
|
||||
reward->iNPC_TypeID = mobid;
|
||||
|
||||
item->sItem = plr->QInven[slot];
|
||||
item->iSlotNum = slot;
|
||||
item->eIL = 2;
|
||||
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||
}
|
||||
|
||||
static int giveMissionReward(CNSocket *sock, int task, int choice=0) {
|
||||
Reward *reward = Rewards[task];
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int nrewards = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (reward->itemIds[i] != 0)
|
||||
nrewards++;
|
||||
}
|
||||
|
||||
// this handles multiple choice rewards in the Academy's Mt. Neverest missions
|
||||
if (choice != 0)
|
||||
nrewards = 1;
|
||||
|
||||
int slots[4];
|
||||
for (int i = 0; i < nrewards; i++) {
|
||||
slots[i] = Items::findFreeSlot(plr);
|
||||
if (slots[i] == -1) {
|
||||
std::cout << "Not enough room to complete task" << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_FAIL, fail);
|
||||
|
||||
fail.iTaskNum = task;
|
||||
fail.iErrorCode = 13; // inventory full
|
||||
|
||||
sock->sendPacket((void*)&fail, P_FE2CL_REP_PC_TASK_END_FAIL, sizeof(sP_FE2CL_REP_PC_TASK_END_FAIL));
|
||||
|
||||
// delete any temp items we might have set
|
||||
for (int j = 0; j < i; j++) {
|
||||
plr->Inven[slots[j]] = { 0, 0, 0, 0 }; // empty
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
plr->Inven[slots[i]] = { 999, 999, 999, 0 }; // temp item; overwritten later
|
||||
}
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + nrewards * sizeof(sItemReward);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE);
|
||||
sP_FE2CL_REP_REWARD_ITEM *resp = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
|
||||
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
||||
|
||||
// don't forget to zero the buffer!
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
// update player
|
||||
plr->money += reward->money;
|
||||
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) { // nano boost for taros
|
||||
int boost = 0;
|
||||
if (Nanos::getNanoBoost(plr)) // for gumballs
|
||||
boost = 1;
|
||||
plr->money += reward->money * (5 + boost) / 25;
|
||||
}
|
||||
|
||||
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) { // nano boost for fm
|
||||
int boost = 0;
|
||||
if (Nanos::getNanoBoost(plr)) // for gumballs
|
||||
boost = 1;
|
||||
updateFusionMatter(sock, reward->fusionmatter * (30 + boost) / 25);
|
||||
} else
|
||||
updateFusionMatter(sock, reward->fusionmatter);
|
||||
|
||||
// simple rewards
|
||||
resp->m_iCandy = plr->money;
|
||||
resp->m_iFusionMatter = plr->fusionmatter;
|
||||
resp->iFatigue = 100; // prevents warning message
|
||||
resp->iFatigue_Level = 1;
|
||||
resp->iItemCnt = nrewards;
|
||||
resp->m_iBatteryN = plr->batteryN;
|
||||
resp->m_iBatteryW = plr->batteryW;
|
||||
|
||||
int offset = 0;
|
||||
|
||||
// choice is actually a bitfield
|
||||
if (choice != 0)
|
||||
offset = (int)log2((int)choice);
|
||||
|
||||
for (int i = 0; i < nrewards; i++) {
|
||||
item[i].sItem.iType = reward->itemTypes[offset+i];
|
||||
item[i].sItem.iID = reward->itemIds[offset+i];
|
||||
item[i].sItem.iOpt = 1;
|
||||
item[i].iSlotNum = slots[i];
|
||||
item[i].eIL = 1;
|
||||
|
||||
// update player inventory, overwriting temporary item
|
||||
plr->Inven[slots[i]] = item[i].sItem;
|
||||
}
|
||||
|
||||
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool endTask(CNSocket *sock, int32_t taskNum, int choice=0) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (Tasks.find(taskNum) == Tasks.end())
|
||||
return false;
|
||||
|
||||
// ugly pointer/reference juggling for the sake of operator overloading...
|
||||
TaskData& task = *Tasks[taskNum];
|
||||
|
||||
// sanity check
|
||||
int i;
|
||||
bool found = false;
|
||||
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == taskNum) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
return false;
|
||||
|
||||
// mission rewards
|
||||
if (Rewards.find(taskNum) != Rewards.end()) {
|
||||
if (giveMissionReward(sock, taskNum, choice) == -1)
|
||||
return false; // we don't want to send anything
|
||||
}
|
||||
// don't take away quest items if we haven't finished the quest
|
||||
|
||||
/*
|
||||
* Update player's active mission data.
|
||||
*
|
||||
* This must be done after all early returns have passed, otherwise we
|
||||
* risk introducing non-atomic changes. For example, failing to finish
|
||||
* a mission due to not having any inventory space could delete the
|
||||
* mission server-side; leading to a desync.
|
||||
*/
|
||||
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == taskNum) {
|
||||
plr->tasks[i] = 0;
|
||||
for (int j = 0; j < 3; j++) {
|
||||
plr->RemainingNPCCount[i][j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Give (or take away) quest items
|
||||
*
|
||||
* Some mission tasks give the player a quest item upon completion.
|
||||
* This is distinct from quest item mob drops.
|
||||
* They can be identified by a counter in the task indicator (ie. 1/1 Gravity Decelerator).
|
||||
* The server is responsible for dropping the correct item.
|
||||
* Yes, this is pretty stupid.
|
||||
*
|
||||
* iSUInstancename is the number of items to give. It is usually negative at the end of
|
||||
* a mission, to clean up its quest items.
|
||||
*/
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (task["m_iSUItem"][i] != 0)
|
||||
dropQuestItem(sock, taskNum, task["m_iSUInstancename"][i], task["m_iSUItem"][i], 0);
|
||||
|
||||
// if it's the last task
|
||||
if (task["m_iSUOutgoingTask"] == 0) {
|
||||
// save completed mission on player
|
||||
saveMission(plr, (int)(task["m_iHMissionID"])-1);
|
||||
|
||||
// if it's a nano mission, reward the nano.
|
||||
if (task["m_iSTNanoID"] != 0)
|
||||
Nanos::addNano(sock, task["m_iSTNanoID"], 0, true);
|
||||
|
||||
// remove current mission
|
||||
plr->CurrentMissionID = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Missions::startTask(Player* plr, int TaskID) {
|
||||
if (Missions::Tasks.find(TaskID) == Missions::Tasks.end()) {
|
||||
std::cout << "[WARN] Player submitted unknown task!?" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
TaskData& task = *Missions::Tasks[TaskID];
|
||||
|
||||
if (task["m_iCTRReqLvMin"] > plr->level) {
|
||||
std::cout << "[WARN] Player tried to start a task above their level" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isMissionCompleted(plr, (int)(task["m_iHMissionID"]) - 1)) {
|
||||
std::cout << "[WARN] Player tried to start an already completed mission" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// client freaks out if nano mission isn't sent first after relogging, so it's easiest to set it here
|
||||
if (task["m_iSTNanoID"] != 0 && plr->tasks[0] != 0) {
|
||||
// lets move task0 to different spot
|
||||
int moveToSlot = 1;
|
||||
for (; moveToSlot < ACTIVE_MISSION_COUNT; moveToSlot++)
|
||||
if (plr->tasks[moveToSlot] == 0)
|
||||
break;
|
||||
|
||||
plr->tasks[moveToSlot] = plr->tasks[0];
|
||||
plr->tasks[0] = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
plr->RemainingNPCCount[moveToSlot][i] = plr->RemainingNPCCount[0][i];
|
||||
plr->RemainingNPCCount[0][i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int i;
|
||||
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == 0) {
|
||||
plr->tasks[i] = TaskID;
|
||||
for (int j = 0; j < 3; j++) {
|
||||
plr->RemainingNPCCount[i][j] = (int)task["m_iCSUNumToKill"][j];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i == ACTIVE_MISSION_COUNT - 1 && plr->tasks[i] != TaskID) {
|
||||
std::cout << "[WARN] Player has more than 6 active missions!?" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void taskStart(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TASK_START* missionData = (sP_CL2FE_REQ_PC_TASK_START*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_SUCC, response);
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (!startTask(plr, missionData->iTaskNum)) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_FAIL, failresp);
|
||||
failresp.iTaskNum = missionData->iTaskNum;
|
||||
failresp.iErrorCode = 1; // unused in the client
|
||||
sock->sendPacket(failresp, P_FE2CL_REP_PC_TASK_START_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
TaskData& task = *Tasks[missionData->iTaskNum];
|
||||
|
||||
// Give player their delivery items at the start, or reset them to 0 at the start.
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (task["m_iSTItemID"][i] != 0)
|
||||
dropQuestItem(sock, missionData->iTaskNum, task["m_iSTItemNumNeeded"][i], task["m_iSTItemID"][i], 0);
|
||||
std::cout << "Mission requested task: " << missionData->iTaskNum << std::endl;
|
||||
response.iTaskNum = missionData->iTaskNum;
|
||||
response.iRemainTime = task["m_iSTGrantTimer"];
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
|
||||
|
||||
// if escort task, assign matching paths to all nearby NPCs
|
||||
if (task["m_iHTaskType"] == 6) {
|
||||
for (ChunkPos& chunkPos : Chunking::getChunksInMap(plr->instanceID)) { // check all NPCs in the instance
|
||||
Chunk* chunk = Chunking::chunks[chunkPos];
|
||||
for (EntityRef ref : chunk->entities) {
|
||||
if (ref.type != EntityType::PLAYER) {
|
||||
BaseNPC* npc = (BaseNPC*)ref.getEntity();
|
||||
NPCPath* path = Transport::findApplicablePath(npc->appearanceData.iNPC_ID, npc->appearanceData.iNPCType, missionData->iTaskNum);
|
||||
if (path != nullptr) {
|
||||
Transport::constructPathNPC(npc->appearanceData.iNPC_ID, path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void taskEnd(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TASK_END* missionData = (sP_CL2FE_REQ_PC_TASK_END*)data->buf;
|
||||
|
||||
TaskData* task = Missions::Tasks[missionData->iTaskNum];
|
||||
|
||||
// handle timed mission failure
|
||||
if (task->task["m_iSTGrantTimer"] > 0 && missionData->iNPC_ID == 0) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
/*
|
||||
* Enemy killing missions
|
||||
* this is gross and should be cleaned up later
|
||||
* once we comb over mission logic more throughly
|
||||
*/
|
||||
bool mobsAreKilled = false;
|
||||
if (task->task["m_iHTaskType"] == 5) {
|
||||
mobsAreKilled = true;
|
||||
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == missionData->iTaskNum) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (plr->RemainingNPCCount[i][j] > 0) {
|
||||
mobsAreKilled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mobsAreKilled) {
|
||||
int failTaskID = task->task["m_iFOutgoingTask"];
|
||||
if (failTaskID != 0) {
|
||||
Missions::quitTask(sock, missionData->iTaskNum, false);
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
if (plr->tasks[i] == missionData->iTaskNum)
|
||||
plr->tasks[i] = failTaskID;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, response);
|
||||
|
||||
response.iTaskNum = missionData->iTaskNum;
|
||||
|
||||
if (!endTask(sock, missionData->iTaskNum, missionData->iBox1Choice)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_END_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_END_SUCC));
|
||||
}
|
||||
|
||||
static void setMission(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
sP_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID* missionData = (sP_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_SET_CURRENT_MISSION_ID, response);
|
||||
response.iCurrentMissionID = missionData->iCurrentMissionID;
|
||||
plr->CurrentMissionID = missionData->iCurrentMissionID;
|
||||
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_SET_CURRENT_MISSION_ID, sizeof(sP_FE2CL_REP_PC_SET_CURRENT_MISSION_ID));
|
||||
}
|
||||
|
||||
static void quitMission(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TASK_STOP* missionData = (sP_CL2FE_REQ_PC_TASK_STOP*)data->buf;
|
||||
quitTask(sock, missionData->iTaskNum, true);
|
||||
}
|
||||
|
||||
void Missions::quitTask(CNSocket* sock, int32_t taskNum, bool manual) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (Tasks.find(taskNum) == Tasks.end())
|
||||
return; // sanity check
|
||||
|
||||
// update player
|
||||
int i;
|
||||
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == taskNum) {
|
||||
plr->tasks[i] = 0;
|
||||
for (int j = 0; j < 3; j++) {
|
||||
plr->RemainingNPCCount[i][j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i == ACTIVE_MISSION_COUNT - 1 && plr->tasks[i] != 0) {
|
||||
std::cout << "[WARN] Player quit non-active mission!?" << std::endl;
|
||||
}
|
||||
// remove current mission
|
||||
plr->CurrentMissionID = 0;
|
||||
|
||||
TaskData& task = *Tasks[taskNum];
|
||||
|
||||
// clean up quest items
|
||||
if (manual) {
|
||||
for (i = 0; i < 3; i++) {
|
||||
if (task["m_iSUItem"][i] == 0 && task["m_iCSUItemID"][i] == 0)
|
||||
continue;
|
||||
|
||||
/*
|
||||
* It's ok to do this only server-side, because the server decides which
|
||||
* slot later items will be placed in.
|
||||
*/
|
||||
for (int j = 0; j < AQINVEN_COUNT; j++)
|
||||
if (plr->QInven[j].iID == task["m_iSUItem"][i] || plr->QInven[j].iID == task["m_iCSUItemID"][i] || plr->QInven[j].iID == task["m_iSTItemID"][i])
|
||||
memset(&plr->QInven[j], 0, sizeof(sItemBase));
|
||||
}
|
||||
} else {
|
||||
for (i = 0; i < 3; i++) {
|
||||
if (task["m_iFItemID"][i] == 0)
|
||||
continue;
|
||||
dropQuestItem(sock, taskNum, task["m_iFItemNumNeeded"][i], task["m_iFItemID"][i], 0);
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_FAIL, failResp);
|
||||
failResp.iErrorCode = 1;
|
||||
failResp.iTaskNum = taskNum;
|
||||
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_TASK_END_FAIL, sizeof(sP_FE2CL_REP_PC_TASK_END_FAIL));
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_STOP_SUCC, response);
|
||||
response.iTaskNum = taskNum;
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_STOP_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_STOP_SUCC));
|
||||
}
|
||||
|
||||
void Missions::updateFusionMatter(CNSocket* sock, int fusion) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
plr->fusionmatter += fusion;
|
||||
|
||||
// there's a much lower FM cap in the Future
|
||||
int fmCap = AvatarGrowth[plr->level]["m_iFMLimit"];
|
||||
if (plr->fusionmatter > fmCap)
|
||||
plr->fusionmatter = fmCap;
|
||||
else if (plr->fusionmatter < 0) // if somehow lowered too far
|
||||
plr->fusionmatter = 0;
|
||||
|
||||
// don't run nano mission logic at level 36
|
||||
if (plr->level >= 36)
|
||||
return;
|
||||
|
||||
// don't give the Blossom nano mission until the player's in the Past
|
||||
if (plr->level == 4 && plr->PCStyle2.iPayzoneFlag == 0)
|
||||
return;
|
||||
|
||||
// check if it is enough for the nano mission
|
||||
int fmNano = AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
|
||||
if (plr->fusionmatter < fmNano)
|
||||
return;
|
||||
|
||||
#ifndef ACADEMY
|
||||
// check if the nano task is already started
|
||||
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
TaskData& task = *Tasks[plr->tasks[i]];
|
||||
if (task["m_iSTNanoID"] != 0)
|
||||
return; // nano mission was already started!
|
||||
}
|
||||
|
||||
// start the nano mission
|
||||
startTask(plr, AvatarGrowth[plr->level]["m_iNanoQuestTaskID"]);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_SUCC, response);
|
||||
response.iTaskNum = AvatarGrowth[plr->level]["m_iNanoQuestTaskID"];
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
|
||||
#else
|
||||
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
|
||||
plr->level++;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response);
|
||||
|
||||
response.iFusionMatter = plr->fusionmatter;
|
||||
response.iLevel = plr->level;
|
||||
|
||||
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, sizeof(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC));
|
||||
#endif
|
||||
|
||||
// play the beam animation for other players
|
||||
INITSTRUCT(sP_FE2CL_PC_EVENT, bcast);
|
||||
bcast.iEventID = 1; // beam effect
|
||||
bcast.iPC_ID = plr->iID;
|
||||
PlayerManager::sendToViewable(sock, (void*)&bcast, P_FE2CL_PC_EVENT, sizeof(sP_FE2CL_PC_EVENT));
|
||||
}
|
||||
|
||||
void Missions::mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
bool missionmob = false;
|
||||
|
||||
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (plr->tasks[i] == 0)
|
||||
continue;
|
||||
|
||||
// tasks[] should always have valid IDs
|
||||
TaskData& task = *Tasks[plr->tasks[i]];
|
||||
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (task["m_iCSUEnemyID"][j] != mobid)
|
||||
continue;
|
||||
|
||||
// acknowledge killing of mission mob...
|
||||
if (task["m_iCSUNumToKill"][j] != 0) {
|
||||
missionmob = true;
|
||||
if (plr->RemainingNPCCount[i][j] > 0) {
|
||||
plr->RemainingNPCCount[i][j]--;
|
||||
}
|
||||
}
|
||||
|
||||
// drop quest item
|
||||
if (task["m_iCSUItemNumNeeded"][j] != 0 && !isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j]) ) {
|
||||
bool drop = rolls[plr->tasks[i]] % 100 < task["m_iSTItemDropRate"][j];
|
||||
if (drop) {
|
||||
dropQuestItem(sock, plr->tasks[i], 1, task["m_iCSUItemID"][j], mobid);
|
||||
|
||||
/*
|
||||
* Workaround: The client has a bug where it only sends a TASK_END request
|
||||
* for the first task of multiple that met their quest item requirements
|
||||
* at the same time. We deal with this by sending TASK_END response packets
|
||||
* proactively and then silently ignoring the extra TASK_END requests it
|
||||
* sends afterwards.
|
||||
*/
|
||||
if (isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j])) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, end);
|
||||
end.iTaskNum = plr->tasks[i];
|
||||
|
||||
if (!endTask(sock, plr->tasks[i]))
|
||||
continue;
|
||||
|
||||
sock->sendPacket(end, P_FE2CL_REP_PC_TASK_END_SUCC);
|
||||
}
|
||||
} else {
|
||||
// fail to drop (itemID == 0)
|
||||
dropQuestItem(sock, plr->tasks[i], 1, 0, mobid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ...but only once
|
||||
// XXX: is it actually necessary to do it this way?
|
||||
if (missionmob) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC, kill);
|
||||
|
||||
kill.iNPCID = mobid;
|
||||
|
||||
sock->sendPacket((void*)&kill, P_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC, sizeof(sP_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC));
|
||||
}
|
||||
}
|
||||
|
||||
void Missions::failInstancedMissions(CNSocket* sock) {
|
||||
// loop through all tasks; if the required instance is being left, "fail" the task
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int taskNum = plr->tasks[i];
|
||||
if (Missions::Tasks.find(taskNum) == Missions::Tasks.end())
|
||||
continue; // sanity check
|
||||
|
||||
TaskData* task = Missions::Tasks[taskNum];
|
||||
if (task->task["m_iRequireInstanceID"] != 0) { // mission is instanced
|
||||
int failTaskID = task->task["m_iFOutgoingTask"];
|
||||
if (failTaskID != 0) {
|
||||
Missions::quitTask(sock, taskNum, false);
|
||||
//plr->tasks[i] = failTaskID; // this causes the client to freak out and send a dupe task
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Missions::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_START, taskStart);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_END, taskEnd);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID, setMission);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_STOP, quitMission);
|
||||
}
|
||||
55
src/Missions.hpp
Normal file
55
src/Missions.hpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "Player.hpp"
|
||||
|
||||
#include "JSON.hpp"
|
||||
|
||||
struct Reward {
|
||||
int32_t id;
|
||||
int32_t itemTypes[4];
|
||||
int32_t itemIds[4];
|
||||
int32_t money;
|
||||
int32_t fusionmatter;
|
||||
|
||||
Reward(int32_t id, nlohmann::json types, nlohmann::json ids, int32_t m, int32_t fm) :
|
||||
id(id), money(m), fusionmatter(fm) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
itemTypes[i] = types[i];
|
||||
itemIds[i] = ids[i];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
struct TaskData {
|
||||
/*
|
||||
* TODO: We'll probably want to keep only the data the server actually needs,
|
||||
* but for now RE/development is much easier if we have everything at
|
||||
* our fingertips.
|
||||
*/
|
||||
nlohmann::json task;
|
||||
|
||||
TaskData(nlohmann::json t) : task(t) {}
|
||||
|
||||
// convenience
|
||||
auto operator[](std::string s) { return task[s]; }
|
||||
};
|
||||
|
||||
namespace Missions {
|
||||
extern std::map<int32_t, Reward*> Rewards;
|
||||
extern std::map<int32_t, TaskData*> Tasks;
|
||||
extern nlohmann::json AvatarGrowth[37];
|
||||
void init();
|
||||
int findQSlot(Player *plr, int id);
|
||||
|
||||
bool startTask(Player* plr, int TaskID);
|
||||
|
||||
// checks if player doesn't have n/n quest items
|
||||
void updateFusionMatter(CNSocket* sock, int fusion);
|
||||
|
||||
void mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls);
|
||||
|
||||
void quitTask(CNSocket* sock, int32_t taskNum, bool manual);
|
||||
|
||||
void failInstancedMissions(CNSocket* sock);
|
||||
}
|
||||
813
src/MobAI.cpp
Normal file
813
src/MobAI.cpp
Normal file
@@ -0,0 +1,813 @@
|
||||
#include "MobAI.hpp"
|
||||
#include "Player.hpp"
|
||||
#include "Racing.hpp"
|
||||
#include "Transport.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "Combat.hpp"
|
||||
#include "Abilities.hpp"
|
||||
#include "Rand.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <limits.h>
|
||||
|
||||
using namespace MobAI;
|
||||
|
||||
bool MobAI::simulateMobs = settings::SIMULATEMOBS;
|
||||
|
||||
static void roamingStep(Mob *mob, time_t currTime);
|
||||
|
||||
/*
|
||||
* Dynamic lerp; distinct from Transport::lerp(). This one doesn't care about height and
|
||||
* only returns the first step, since the rest will need to be recalculated anyway if chasing player.
|
||||
*/
|
||||
static std::pair<int,int> lerp(int x1, int y1, int x2, int y2, int speed) {
|
||||
std::pair<int,int> ret = {x1, y1};
|
||||
|
||||
if (speed == 0)
|
||||
return ret;
|
||||
|
||||
int distance = hypot(x1 - x2, y1 - y2);
|
||||
|
||||
if (distance > speed) {
|
||||
|
||||
int lerps = distance / speed;
|
||||
|
||||
// interpolate only the first point
|
||||
float frac = 1.0f / lerps;
|
||||
|
||||
ret.first = (x1 + (x2 - x1) * frac);
|
||||
ret.second = (y1 + (y2 - y1) * frac);
|
||||
} else {
|
||||
ret.first = x2;
|
||||
ret.second = y2;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void MobAI::clearDebuff(Mob *mob) {
|
||||
mob->skillStyle = -1;
|
||||
mob->appearanceData.iConditionBitFlag = 0;
|
||||
mob->unbuffTimes.clear();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
|
||||
pkt1.eCT = 2;
|
||||
pkt1.iID = mob->appearanceData.iNPC_ID;
|
||||
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
|
||||
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
||||
}
|
||||
|
||||
void MobAI::followToCombat(Mob *mob) {
|
||||
if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->type == EntityType::MOB) {
|
||||
Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (leadMob->groupMember[i] == 0)
|
||||
break;
|
||||
|
||||
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
|
||||
continue;
|
||||
}
|
||||
Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]];
|
||||
|
||||
if (followerMob->state != MobState::ROAMING) // only roaming mobs should transition to combat
|
||||
continue;
|
||||
|
||||
enterCombat(mob->target, followerMob);
|
||||
}
|
||||
|
||||
if (leadMob->state != MobState::ROAMING)
|
||||
return;
|
||||
|
||||
enterCombat(mob->target, leadMob);
|
||||
}
|
||||
}
|
||||
|
||||
void MobAI::groupRetreat(Mob *mob) {
|
||||
if (NPCManager::NPCs.find(mob->groupLeader) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupLeader]->type != EntityType::MOB)
|
||||
return;
|
||||
|
||||
Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (leadMob->groupMember[i] == 0)
|
||||
break;
|
||||
|
||||
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
|
||||
continue;
|
||||
}
|
||||
Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]];
|
||||
|
||||
if (followerMob->state != MobState::COMBAT)
|
||||
continue;
|
||||
|
||||
followerMob->target = nullptr;
|
||||
followerMob->state = MobState::RETREAT;
|
||||
clearDebuff(followerMob);
|
||||
}
|
||||
|
||||
if (leadMob->state != MobState::COMBAT)
|
||||
return;
|
||||
|
||||
leadMob->target = nullptr;
|
||||
leadMob->state = MobState::RETREAT;
|
||||
clearDebuff(leadMob);
|
||||
}
|
||||
|
||||
/*
|
||||
* Aggro on nearby players.
|
||||
* Even if they're in range, we can't assume they're all in the same one chunk
|
||||
* as the mob, since it might be near a chunk boundary.
|
||||
*/
|
||||
bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
|
||||
CNSocket *closest = nullptr;
|
||||
int closestDistance = INT_MAX;
|
||||
|
||||
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
|
||||
Chunk* chunk = *it;
|
||||
for (const EntityRef& ref : chunk->entities) {
|
||||
// TODO: support targetting other CombatNPCs
|
||||
if (ref.type != EntityType::PLAYER)
|
||||
continue;
|
||||
|
||||
CNSocket *s = ref.sock;
|
||||
Player *plr = PlayerManager::getPlayer(s);
|
||||
|
||||
if (plr->HP <= 0 || plr->onMonkey)
|
||||
continue;
|
||||
|
||||
int mobRange = mob->sightRange;
|
||||
|
||||
if (plr->iConditionBitFlag & CSB_BIT_UP_STEALTH
|
||||
|| Racing::EPRaces.find(s) != Racing::EPRaces.end())
|
||||
mobRange /= 3;
|
||||
|
||||
// 0.33x - 1.66x the range
|
||||
int levelDifference = plr->level - mob->level;
|
||||
if (levelDifference > -10)
|
||||
mobRange = levelDifference < 10 ? mobRange - (levelDifference * mobRange / 15) : mobRange / 3;
|
||||
|
||||
if (mob->state != MobState::ROAMING && plr->inCombat) // freshly out of aggro mobs
|
||||
mobRange = mob->sightRange * 2; // should not be impacted by the above
|
||||
|
||||
if (plr->iSpecialState & (CN_SPECIAL_STATE_FLAG__INVISIBLE|CN_SPECIAL_STATE_FLAG__INVULNERABLE))
|
||||
mobRange = -1;
|
||||
|
||||
// height is relevant for aggro distance because of platforming
|
||||
int xyDistance = hypot(mob->x - plr->x, mob->y - plr->y);
|
||||
int distance = hypot(xyDistance, (mob->z - plr->z) * 2); // difference in Z counts twice
|
||||
|
||||
if (distance > mobRange || distance > closestDistance)
|
||||
continue;
|
||||
|
||||
// found a player
|
||||
closest = s;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
if (closest != nullptr) {
|
||||
// found closest player. engage.
|
||||
enterCombat(closest, mob);
|
||||
|
||||
if (mob->groupLeader != 0)
|
||||
followToCombat(mob);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int style) {
|
||||
Player *plr = PlayerManager::getPlayer(mob->target);
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT) + targetData[0] * sizeof(sCAttackResult);
|
||||
|
||||
// validate response packet
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT), targetData[0], sizeof(sCAttackResult))) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_CORRUPTION_HIT packet size" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_NPC_SKILL_CORRUPTION_HIT *resp = (sP_FE2CL_NPC_SKILL_CORRUPTION_HIT*)respbuf;
|
||||
sCAttackResult *respdata = (sCAttackResult*)(respbuf+sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT));
|
||||
|
||||
resp->iNPC_ID = mob->appearanceData.iNPC_ID;
|
||||
resp->iSkillID = skillID;
|
||||
resp->iStyle = style;
|
||||
resp->iValue1 = plr->x;
|
||||
resp->iValue2 = plr->y;
|
||||
resp->iValue3 = plr->z;
|
||||
resp->iTargetCnt = targetData[0];
|
||||
|
||||
for (int i = 0; i < targetData[0]; i++) {
|
||||
CNSocket *sock = nullptr;
|
||||
Player *plr = nullptr;
|
||||
|
||||
for (auto& pair : PlayerManager::players) {
|
||||
if (pair.second->iID == targetData[i+1]) {
|
||||
sock = pair.first;
|
||||
plr = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
if (plr == nullptr) {
|
||||
std::cout << "[WARN] dealCorruption: player ID not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
respdata[i].eCT = 1;
|
||||
respdata[i].iID = plr->iID;
|
||||
respdata[i].bProtected = 0;
|
||||
|
||||
respdata[i].iActiveNanoSlotNum = -1;
|
||||
for (int n = 0; n < 3; n++)
|
||||
if (plr->activeNano == plr->equippedNanos[n])
|
||||
respdata[i].iActiveNanoSlotNum = n;
|
||||
respdata[i].iNanoID = plr->activeNano;
|
||||
|
||||
int style2 = Nanos::nanoStyle(plr->activeNano);
|
||||
if (style2 == -1) { // no nano
|
||||
respdata[i].iHitFlag = 8;
|
||||
respdata[i].iDamage = Nanos::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
|
||||
} else if (style == style2) {
|
||||
respdata[i].iHitFlag = 8; // tie
|
||||
respdata[i].iDamage = 0;
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
|
||||
} else if (style - style2 == 1 || style2 - style == 2) {
|
||||
respdata[i].iHitFlag = 4; // win
|
||||
respdata[i].iDamage = 0;
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45;
|
||||
if (plr->Nanos[plr->activeNano].iStamina > 150)
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 150;
|
||||
// fire damage power disguised as a corruption attack back at the enemy
|
||||
std::vector<int> targetData2 = {1, mob->appearanceData.iNPC_ID, 0, 0, 0};
|
||||
for (auto& pwr : Nanos::NanoPowers)
|
||||
if (pwr.skillType == EST_DAMAGE)
|
||||
pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200);
|
||||
} else {
|
||||
respdata[i].iHitFlag = 16; // lose
|
||||
respdata[i].iDamage = Nanos::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina -= 90;
|
||||
if (plr->Nanos[plr->activeNano].iStamina < 0) {
|
||||
respdata[i].bNanoDeactive = 1;
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
|
||||
plr->HP -= respdata[i].iDamage;
|
||||
|
||||
respdata[i].iHP = plr->HP;
|
||||
respdata[i].iConditionBitFlag = plr->iConditionBitFlag;
|
||||
|
||||
if (plr->HP <= 0) {
|
||||
mob->target = nullptr;
|
||||
mob->state = MobState::RETREAT;
|
||||
if (!aggroCheck(mob, getTime())) {
|
||||
clearDebuff(mob);
|
||||
if (mob->groupLeader != 0)
|
||||
groupRetreat(mob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_CORRUPTION_HIT, resplen);
|
||||
}
|
||||
|
||||
static void useAbilities(Mob *mob, time_t currTime) {
|
||||
/*
|
||||
* targetData approach
|
||||
* first integer is the count
|
||||
* second to fifth integers are IDs, these can be either player iID or mob's iID
|
||||
* whether the skill targets players or mobs is determined by the skill packet being fired
|
||||
*/
|
||||
Player *plr = PlayerManager::getPlayer(mob->target);
|
||||
|
||||
if (mob->skillStyle >= 0) { // corruption hit
|
||||
int skillID = (int)mob->data["m_iCorruptionType"];
|
||||
std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
|
||||
int temp = mob->skillStyle;
|
||||
mob->skillStyle = -3; // corruption cooldown
|
||||
mob->nextAttack = currTime + 1000;
|
||||
dealCorruption(mob, targetData, skillID, temp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mob->skillStyle == -2) { // eruption hit
|
||||
int skillID = (int)mob->data["m_iMegaType"];
|
||||
std::vector<int> targetData = {0, 0, 0, 0, 0};
|
||||
|
||||
// find the players within range of eruption
|
||||
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
|
||||
Chunk* chunk = *it;
|
||||
for (const EntityRef& ref : chunk->entities) {
|
||||
// TODO: see aggroCheck()
|
||||
if (ref.type != EntityType::PLAYER)
|
||||
continue;
|
||||
|
||||
CNSocket *s= ref.sock;
|
||||
Player *plr = PlayerManager::getPlayer(s);
|
||||
|
||||
if (plr->HP <= 0)
|
||||
continue;
|
||||
|
||||
int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y);
|
||||
if (distance < Nanos::SkillTable[skillID].effectArea) {
|
||||
targetData[0] += 1;
|
||||
targetData[targetData[0]] = plr->iID;
|
||||
if (targetData[0] > 3) // make sure not to have more than 4
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& pwr : Combat::MobPowers)
|
||||
if (pwr.skillType == Nanos::SkillTable[skillID].skillType)
|
||||
pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]);
|
||||
mob->skillStyle = -3; // eruption cooldown
|
||||
mob->nextAttack = currTime + 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mob->skillStyle == -3) { // cooldown expires
|
||||
mob->skillStyle = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
int random = Rand::rand(2000) * 1000;
|
||||
int prob1 = (int)mob->data["m_iActiveSkill1Prob"]; // active skill probability
|
||||
int prob2 = (int)mob->data["m_iCorruptionTypeProb"]; // corruption probability
|
||||
int prob3 = (int)mob->data["m_iMegaTypeProb"]; // eruption probability
|
||||
|
||||
if (random < prob1) { // active skill hit
|
||||
int skillID = (int)mob->data["m_iActiveSkill1"];
|
||||
std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
|
||||
for (auto& pwr : Combat::MobPowers)
|
||||
if (pwr.skillType == Nanos::SkillTable[skillID].skillType) {
|
||||
if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag))
|
||||
return; // prevent debuffing a player twice
|
||||
pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]);
|
||||
}
|
||||
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
|
||||
return;
|
||||
}
|
||||
|
||||
if (random < prob1 + prob2) { // corruption windup
|
||||
int skillID = (int)mob->data["m_iCorruptionType"];
|
||||
INITSTRUCT(sP_FE2CL_NPC_SKILL_CORRUPTION_READY, pkt);
|
||||
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
|
||||
pkt.iSkillID = skillID;
|
||||
pkt.iValue1 = plr->x;
|
||||
pkt.iValue2 = plr->y;
|
||||
pkt.iValue3 = plr->z;
|
||||
mob->skillStyle = Nanos::nanoStyle(plr->activeNano) - 1;
|
||||
if (mob->skillStyle == -1)
|
||||
mob->skillStyle = 2;
|
||||
if (mob->skillStyle == -2)
|
||||
mob->skillStyle = Rand::rand(3);
|
||||
pkt.iStyle = mob->skillStyle;
|
||||
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_SKILL_CORRUPTION_READY, sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_READY));
|
||||
mob->nextAttack = currTime + 1800;
|
||||
return;
|
||||
}
|
||||
|
||||
if (random < prob1 + prob2 + prob3) { // eruption windup
|
||||
int skillID = (int)mob->data["m_iMegaType"];
|
||||
INITSTRUCT(sP_FE2CL_NPC_SKILL_READY, pkt);
|
||||
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
|
||||
pkt.iSkillID = skillID;
|
||||
pkt.iValue1 = mob->hitX = plr->x;
|
||||
pkt.iValue2 = mob->hitY = plr->y;
|
||||
pkt.iValue3 = mob->hitZ = plr->z;
|
||||
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_SKILL_READY, sizeof(sP_FE2CL_NPC_SKILL_READY));
|
||||
mob->nextAttack = currTime + 1800;
|
||||
mob->skillStyle = -2;
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void MobAI::enterCombat(CNSocket *sock, Mob *mob) {
|
||||
mob->target = sock;
|
||||
mob->state = MobState::COMBAT;
|
||||
mob->nextMovement = getTime();
|
||||
mob->nextAttack = 0;
|
||||
|
||||
mob->roamX = mob->x;
|
||||
mob->roamY = mob->y;
|
||||
mob->roamZ = mob->z;
|
||||
|
||||
int skillID = (int)mob->data["m_iPassiveBuff"]; // cast passive
|
||||
std::vector<int> targetData = {1, mob->appearanceData.iNPC_ID, 0, 0, 0};
|
||||
for (auto& pwr : Combat::MobPowers)
|
||||
if (pwr.skillType == Nanos::SkillTable[skillID].skillType)
|
||||
pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]);
|
||||
|
||||
for (NPCEvent& event : NPCManager::NPCEvents) // trigger an ON_COMBAT
|
||||
if (event.trigger == ON_COMBAT && event.npcType == mob->appearanceData.iNPCType)
|
||||
event.handler(sock, mob);
|
||||
}
|
||||
|
||||
static void drainMobHP(Mob *mob, int amount) {
|
||||
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
|
||||
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
||||
|
||||
pkt->iID = mob->appearanceData.iNPC_ID;
|
||||
pkt->eCT = 4; // mob
|
||||
pkt->iTB_ID = ECSB_BOUNDINGBALL;
|
||||
|
||||
drain->eCT = 4;
|
||||
drain->iID = mob->appearanceData.iNPC_ID;
|
||||
drain->iDamage = amount;
|
||||
drain->iHP = mob->appearanceData.iHP -= amount;
|
||||
|
||||
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
|
||||
|
||||
if (mob->appearanceData.iHP <= 0)
|
||||
Combat::killMob(mob->target, mob);
|
||||
}
|
||||
|
||||
static void deadStep(Mob *mob, time_t currTime) {
|
||||
// despawn the mob after a short delay
|
||||
if (mob->killedTime != 0 && !mob->despawned && currTime - mob->killedTime > 2000) {
|
||||
mob->despawned = true;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
|
||||
|
||||
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
|
||||
|
||||
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
|
||||
|
||||
// if it was summoned, mark it for removal
|
||||
if (mob->summoned) {
|
||||
std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl;
|
||||
NPCManager::queueNPCRemoval(mob->appearanceData.iNPC_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// pre-set spawn coordinates if not marked for removal
|
||||
mob->x = mob->spawnX;
|
||||
mob->y = mob->spawnY;
|
||||
mob->z = mob->spawnZ;
|
||||
}
|
||||
|
||||
// to guide their groupmates, group leaders still need to move despite being dead
|
||||
if (mob->groupLeader == mob->appearanceData.iNPC_ID)
|
||||
roamingStep(mob, currTime);
|
||||
|
||||
if (mob->killedTime != 0 && currTime - mob->killedTime < mob->regenTime * 100)
|
||||
return;
|
||||
|
||||
std::cout << "respawning mob " << mob->appearanceData.iNPC_ID << " with HP = " << mob->maxHealth << std::endl;
|
||||
|
||||
mob->appearanceData.iHP = mob->maxHealth;
|
||||
mob->state = MobState::ROAMING;
|
||||
|
||||
// if mob is a group leader/follower, spawn where the group is.
|
||||
if (mob->groupLeader != 0) {
|
||||
if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->type == EntityType::MOB) {
|
||||
Mob* leaderMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
|
||||
mob->x = leaderMob->x + mob->offsetX;
|
||||
mob->y = leaderMob->y + mob->offsetY;
|
||||
mob->z = leaderMob->z;
|
||||
} else {
|
||||
std::cout << "[WARN] deadStep: mob cannot find it's leader!" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_NPC_NEW, pkt);
|
||||
|
||||
pkt.NPCAppearanceData = mob->appearanceData;
|
||||
pkt.NPCAppearanceData.iX = mob->x;
|
||||
pkt.NPCAppearanceData.iY = mob->y;
|
||||
pkt.NPCAppearanceData.iZ = mob->z;
|
||||
|
||||
// notify all nearby players
|
||||
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
|
||||
}
|
||||
|
||||
static void combatStep(Mob *mob, time_t currTime) {
|
||||
assert(mob->target != nullptr);
|
||||
|
||||
// lose aggro if the player lost connection
|
||||
if (PlayerManager::players.find(mob->target) == PlayerManager::players.end()) {
|
||||
mob->target = nullptr;
|
||||
mob->state = MobState::RETREAT;
|
||||
if (!aggroCheck(mob, currTime)) {
|
||||
clearDebuff(mob);
|
||||
if (mob->groupLeader != 0)
|
||||
groupRetreat(mob);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(mob->target);
|
||||
|
||||
// lose aggro if the player became invulnerable or died
|
||||
if (plr->HP <= 0
|
||||
|| (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) {
|
||||
mob->target = nullptr;
|
||||
mob->state = MobState::RETREAT;
|
||||
if (!aggroCheck(mob, currTime)) {
|
||||
clearDebuff(mob);
|
||||
if (mob->groupLeader != 0)
|
||||
groupRetreat(mob);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// drain
|
||||
if (mob->skillStyle < 0 && (mob->lastDrainTime == 0 || currTime - mob->lastDrainTime >= 1000)
|
||||
&& mob->appearanceData.iConditionBitFlag & CSB_BIT_BOUNDINGBALL) {
|
||||
drainMobHP(mob, mob->maxHealth / 20); // lose 5% every second
|
||||
mob->lastDrainTime = currTime;
|
||||
}
|
||||
|
||||
// if drain killed the mob, return early
|
||||
if (mob->appearanceData.iHP <= 0)
|
||||
return;
|
||||
|
||||
// unbuffing
|
||||
std::unordered_map<int32_t, time_t>::iterator it = mob->unbuffTimes.begin();
|
||||
while (it != mob->unbuffTimes.end()) {
|
||||
|
||||
if (currTime >= it->second) {
|
||||
mob->appearanceData.iConditionBitFlag &= ~it->first;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
|
||||
pkt1.eCT = 2;
|
||||
pkt1.iID = mob->appearanceData.iNPC_ID;
|
||||
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
|
||||
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
||||
|
||||
it = mob->unbuffTimes.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
|
||||
// skip attack if stunned or asleep
|
||||
if (mob->appearanceData.iConditionBitFlag & (CSB_BIT_STUN|CSB_BIT_MEZ)) {
|
||||
mob->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
|
||||
return;
|
||||
}
|
||||
|
||||
int distance = hypot(plr->x - mob->x, plr->y - mob->y);
|
||||
int mobRange = (int)mob->data["m_iAtkRange"] + (int)mob->data["m_iRadius"];
|
||||
|
||||
if (currTime >= mob->nextAttack) {
|
||||
if (mob->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance.
|
||||
useAbilities(mob, currTime);
|
||||
if (mob->target == nullptr)
|
||||
return;
|
||||
}
|
||||
|
||||
int distanceToTravel = INT_MAX;
|
||||
int speed = mob->speed;
|
||||
// movement logic: move when out of range but don't move while casting a skill
|
||||
if (distance > mobRange && mob->skillStyle == -1) {
|
||||
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
|
||||
return;
|
||||
mob->nextMovement = currTime + 400;
|
||||
if (currTime >= mob->nextAttack)
|
||||
mob->nextAttack = 0;
|
||||
|
||||
// halve movement speed if snared
|
||||
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED)
|
||||
speed /= 2;
|
||||
|
||||
int targetX = plr->x;
|
||||
int targetY = plr->y;
|
||||
if (mob->groupLeader != 0) {
|
||||
targetX += mob->offsetX*distance/(mob->idleRange + 1);
|
||||
targetY += mob->offsetY*distance/(mob->idleRange + 1);
|
||||
}
|
||||
|
||||
distanceToTravel = std::min(distance-mobRange+1, speed*2/5);
|
||||
auto targ = lerp(mob->x, mob->y, targetX, targetY, distanceToTravel);
|
||||
if (distanceToTravel < speed*2/5 && currTime >= mob->nextAttack)
|
||||
mob->nextAttack = 0;
|
||||
|
||||
NPCManager::updateNPCPosition(mob->appearanceData.iNPC_ID, targ.first, targ.second, mob->z, mob->instanceID, mob->appearanceData.iAngle);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
||||
|
||||
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
|
||||
pkt.iSpeed = speed;
|
||||
pkt.iToX = mob->x = targ.first;
|
||||
pkt.iToY = mob->y = targ.second;
|
||||
pkt.iToZ = plr->z;
|
||||
pkt.iMoveStyle = 1;
|
||||
|
||||
// notify all nearby players
|
||||
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
||||
}
|
||||
|
||||
/* attack logic
|
||||
* 2/5 represents 400 ms which is the time interval mobs use per movement logic step
|
||||
* if the mob is one move interval away, we should just start attacking anyways.
|
||||
*/
|
||||
if (distance <= mobRange || distanceToTravel < speed*2/5) {
|
||||
if (mob->nextAttack == 0 || currTime >= mob->nextAttack) {
|
||||
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
|
||||
Combat::npcAttackPc(mob, currTime);
|
||||
}
|
||||
}
|
||||
|
||||
// retreat if the player leaves combat range
|
||||
int xyDistance = hypot(plr->x - mob->roamX, plr->y - mob->roamY);
|
||||
distance = hypot(xyDistance, plr->z - mob->roamZ);
|
||||
if (distance >= mob->data["m_iCombatRange"]) {
|
||||
mob->target = nullptr;
|
||||
mob->state = MobState::RETREAT;
|
||||
clearDebuff(mob);
|
||||
if (mob->groupLeader != 0)
|
||||
groupRetreat(mob);
|
||||
}
|
||||
}
|
||||
|
||||
void MobAI::incNextMovement(Mob *mob, time_t currTime) {
|
||||
if (currTime == 0)
|
||||
currTime = getTime();
|
||||
|
||||
int delay = (int)mob->data["m_iDelayTime"] * 1000;
|
||||
mob->nextMovement = currTime + delay/2 + Rand::rand(delay/2);
|
||||
}
|
||||
|
||||
static void roamingStep(Mob *mob, time_t currTime) {
|
||||
/*
|
||||
* We reuse nextAttack to avoid scanning for players all the time, but to still
|
||||
* do so more often than if we waited for nextMovement (which is way too slow).
|
||||
* In the case of group leaders, this step will be called by dead mobs, so disable attack.
|
||||
*/
|
||||
if (mob->state != MobState::DEAD && (mob->nextAttack == 0 || currTime >= mob->nextAttack)) {
|
||||
mob->nextAttack = currTime + 500;
|
||||
if (aggroCheck(mob, currTime))
|
||||
return;
|
||||
}
|
||||
|
||||
// no random roaming if the mob already has a set path
|
||||
if (mob->staticPath)
|
||||
return;
|
||||
|
||||
if (mob->groupLeader != 0 && mob->groupLeader != mob->appearanceData.iNPC_ID) // don't roam by yourself without group leader
|
||||
return;
|
||||
|
||||
/*
|
||||
* mob->nextMovement is also updated whenever the path queue is traversed in
|
||||
* Transport::stepNPCPathing() (which ticks at a higher frequency than nextMovement),
|
||||
* so we don't have to check if there's already entries in the queue since we know there won't be.
|
||||
*/
|
||||
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
|
||||
return;
|
||||
incNextMovement(mob, currTime);
|
||||
|
||||
int xStart = mob->spawnX - mob->idleRange/2;
|
||||
int yStart = mob->spawnY - mob->idleRange/2;
|
||||
int speed = mob->speed;
|
||||
|
||||
// some mobs don't move (and we mustn't divide/modulus by zero)
|
||||
if (mob->idleRange == 0 || speed == 0)
|
||||
return;
|
||||
|
||||
int farX, farY, distance;
|
||||
int minDistance = mob->idleRange / 2;
|
||||
|
||||
// pick a random destination
|
||||
farX = xStart + Rand::rand(mob->idleRange);
|
||||
farY = yStart + Rand::rand(mob->idleRange);
|
||||
|
||||
distance = std::abs(std::max(farX - mob->x, farY - mob->y));
|
||||
if (distance == 0)
|
||||
distance += 1; // hack to avoid FPE
|
||||
|
||||
// if it's too short a walk, go further in that direction
|
||||
farX = mob->x + (farX - mob->x) * minDistance / distance;
|
||||
farY = mob->y + (farY - mob->y) * minDistance / distance;
|
||||
|
||||
// but don't got out of bounds
|
||||
farX = std::clamp(farX, xStart, xStart + mob->idleRange);
|
||||
farY = std::clamp(farY, yStart, yStart + mob->idleRange);
|
||||
|
||||
// halve movement speed if snared
|
||||
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED)
|
||||
speed /= 2;
|
||||
|
||||
std::queue<Vec3> queue;
|
||||
Vec3 from = { mob->x, mob->y, mob->z };
|
||||
Vec3 to = { farX, farY, mob->z };
|
||||
|
||||
// add a route to the queue; to be processed in Transport::stepNPCPathing()
|
||||
Transport::lerp(&queue, from, to, speed);
|
||||
Transport::NPCQueues[mob->appearanceData.iNPC_ID] = queue;
|
||||
|
||||
if (mob->groupLeader != 0 && mob->groupLeader == mob->appearanceData.iNPC_ID) {
|
||||
// make followers follow this npc.
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (mob->groupMember[i] == 0)
|
||||
break;
|
||||
|
||||
if (NPCManager::NPCs.find(mob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupMember[i]]->type != EntityType::MOB) {
|
||||
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::queue<Vec3> queue2;
|
||||
Mob* followerMob = (Mob*)NPCManager::NPCs[mob->groupMember[i]];
|
||||
from = { followerMob->x, followerMob->y, followerMob->z };
|
||||
to = { farX + followerMob->offsetX, farY + followerMob->offsetY, followerMob->z };
|
||||
Transport::lerp(&queue2, from, to, speed);
|
||||
Transport::NPCQueues[followerMob->appearanceData.iNPC_ID] = queue2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void retreatStep(Mob *mob, time_t currTime) {
|
||||
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
|
||||
return;
|
||||
|
||||
mob->nextMovement = currTime + 400;
|
||||
|
||||
// distance between spawn point and current location
|
||||
int distance = hypot(mob->x - mob->roamX, mob->y - mob->roamY);
|
||||
|
||||
//if (distance > mob->data["m_iIdleRange"]) {
|
||||
if (distance > 10) {
|
||||
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
||||
|
||||
auto targ = lerp(mob->x, mob->y, mob->roamX, mob->roamY, (int)mob->speed*4/5);
|
||||
|
||||
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
|
||||
pkt.iSpeed = (int)mob->speed * 2;
|
||||
pkt.iToX = mob->x = targ.first;
|
||||
pkt.iToY = mob->y = targ.second;
|
||||
pkt.iToZ = mob->z = mob->spawnZ;
|
||||
pkt.iMoveStyle = 1;
|
||||
|
||||
// notify all nearby players
|
||||
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
||||
}
|
||||
|
||||
// if we got there
|
||||
//if (distance <= mob->data["m_iIdleRange"]) {
|
||||
if (distance <= 10) { // retreat back to the spawn point
|
||||
mob->state = MobState::ROAMING;
|
||||
mob->appearanceData.iHP = mob->maxHealth;
|
||||
mob->killedTime = 0;
|
||||
mob->nextAttack = 0;
|
||||
mob->appearanceData.iConditionBitFlag = 0;
|
||||
|
||||
// cast a return home heal spell, this is the right way(tm)
|
||||
std::vector<int> targetData = {1, 0, 0, 0, 0};
|
||||
for (auto& pwr : Combat::MobPowers)
|
||||
if (pwr.skillType == Nanos::SkillTable[110].skillType)
|
||||
pwr.handle(mob, targetData, 110, Nanos::SkillTable[110].durationTime[0], Nanos::SkillTable[110].powerIntensity[0]);
|
||||
// clear outlying debuffs
|
||||
clearDebuff(mob);
|
||||
}
|
||||
}
|
||||
|
||||
void MobAI::step(CombatNPC *npc, time_t currTime) {
|
||||
assert(npc->type == EntityType::MOB);
|
||||
auto mob = (Mob*)npc;
|
||||
|
||||
if (mob->playersInView < 0)
|
||||
std::cout << "[WARN] Weird playerview value " << mob->playersInView << std::endl;
|
||||
|
||||
// skip mob movement and combat if disabled or not in view
|
||||
if ((!simulateMobs || mob->playersInView == 0) && mob->state != MobState::DEAD
|
||||
&& mob->state != MobState::RETREAT)
|
||||
return;
|
||||
|
||||
switch (mob->state) {
|
||||
case MobState::INACTIVE:
|
||||
// no-op
|
||||
break;
|
||||
case MobState::ROAMING:
|
||||
roamingStep(mob, currTime);
|
||||
break;
|
||||
case MobState::COMBAT:
|
||||
combatStep(mob, currTime);
|
||||
break;
|
||||
case MobState::RETREAT:
|
||||
retreatStep(mob, currTime);
|
||||
break;
|
||||
case MobState::DEAD:
|
||||
deadStep(mob, currTime);
|
||||
break;
|
||||
}
|
||||
}
|
||||
107
src/MobAI.hpp
Normal file
107
src/MobAI.hpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
|
||||
enum class MobState {
|
||||
INACTIVE,
|
||||
ROAMING,
|
||||
COMBAT,
|
||||
RETREAT,
|
||||
DEAD
|
||||
};
|
||||
|
||||
namespace MobAI {
|
||||
// needs to be declared before Mob's constructor
|
||||
void step(CombatNPC*, time_t);
|
||||
};
|
||||
|
||||
struct Mob : public CombatNPC {
|
||||
// general
|
||||
MobState state = MobState::INACTIVE;
|
||||
|
||||
std::unordered_map<int32_t,time_t> unbuffTimes = {};
|
||||
|
||||
// dead
|
||||
time_t killedTime = 0;
|
||||
time_t regenTime = 0;
|
||||
bool summoned = false;
|
||||
bool despawned = false; // for the sake of death animations
|
||||
|
||||
// roaming
|
||||
int idleRange = 0;
|
||||
const int sightRange = 0;
|
||||
time_t nextMovement = 0;
|
||||
bool staticPath = false;
|
||||
int roamX = 0, roamY = 0, roamZ = 0;
|
||||
|
||||
// combat
|
||||
CNSocket *target = nullptr;
|
||||
time_t nextAttack = 0;
|
||||
time_t lastDrainTime = 0;
|
||||
int skillStyle = -1; // -1 for nothing, 0-2 for corruption, -2 for eruption
|
||||
int hitX = 0, hitY = 0, hitZ = 0; // for use in ability targeting
|
||||
|
||||
// group
|
||||
int groupLeader = 0;
|
||||
int offsetX = 0, offsetY = 0;
|
||||
int groupMember[4] = {};
|
||||
|
||||
// for optimizing away AI in empty chunks
|
||||
int playersInView = 0;
|
||||
|
||||
// temporary; until we're sure what's what
|
||||
nlohmann::json data = {};
|
||||
|
||||
Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
|
||||
: CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]),
|
||||
sightRange(d["m_iSightRange"]) {
|
||||
state = MobState::ROAMING;
|
||||
|
||||
data = d;
|
||||
|
||||
speed = data["m_iRunSpeed"];
|
||||
regenTime = data["m_iRegenTime"];
|
||||
idleRange = (int)data["m_iIdleRange"];
|
||||
level = data["m_iNpcLevel"];
|
||||
|
||||
roamX = spawnX = x;
|
||||
roamY = spawnY = y;
|
||||
roamZ = spawnZ = z;
|
||||
|
||||
offsetX = 0;
|
||||
offsetY = 0;
|
||||
|
||||
appearanceData.iConditionBitFlag = 0;
|
||||
|
||||
// NOTE: there appear to be discrepancies in the dump
|
||||
appearanceData.iHP = maxHealth;
|
||||
|
||||
type = EntityType::MOB;
|
||||
_stepAI = MobAI::step;
|
||||
}
|
||||
|
||||
// constructor for /summon
|
||||
Mob(int x, int y, int z, uint64_t iID, int t, nlohmann::json d, int32_t id)
|
||||
: Mob(x, y, z, 0, iID, t, d, id) {
|
||||
summoned = true; // will be despawned and deallocated when killed
|
||||
}
|
||||
|
||||
~Mob() {}
|
||||
|
||||
auto operator[](std::string s) {
|
||||
return data[s];
|
||||
}
|
||||
};
|
||||
|
||||
namespace MobAI {
|
||||
extern bool simulateMobs;
|
||||
|
||||
// TODO: make this internal later
|
||||
void incNextMovement(Mob *mob, time_t currTime=0);
|
||||
bool aggroCheck(Mob *mob, time_t currTime);
|
||||
void clearDebuff(Mob *mob);
|
||||
void followToCombat(Mob *mob);
|
||||
void groupRetreat(Mob *mob);
|
||||
void enterCombat(CNSocket *sock, Mob *mob);
|
||||
}
|
||||
4
src/NPC.hpp
Normal file
4
src/NPC.hpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#include "Chunking.hpp"
|
||||
#include "Entities.hpp"
|
||||
377
src/NPCManager.cpp
Normal file
377
src/NPCManager.cpp
Normal file
@@ -0,0 +1,377 @@
|
||||
#include "NPCManager.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "Combat.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Chunking.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "TableData.hpp"
|
||||
#include "Groups.hpp"
|
||||
#include "Racing.hpp"
|
||||
#include "Vendors.hpp"
|
||||
#include "Abilities.hpp"
|
||||
#include "Rand.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <list>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <assert.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include "JSON.hpp"
|
||||
|
||||
using namespace NPCManager;
|
||||
|
||||
std::unordered_map<int32_t, BaseNPC*> NPCManager::NPCs;
|
||||
std::map<int32_t, WarpLocation> NPCManager::Warps;
|
||||
std::vector<WarpLocation> NPCManager::RespawnPoints;
|
||||
nlohmann::json NPCManager::NPCData;
|
||||
|
||||
static std::queue<int32_t> RemovalQueue;
|
||||
|
||||
/*
|
||||
* Initialized at the end of TableData::init().
|
||||
* This allows us to summon and kill mobs in arbitrary order without
|
||||
* NPC ID collisions.
|
||||
*/
|
||||
int32_t NPCManager::nextId;
|
||||
|
||||
void NPCManager::destroyNPC(int32_t id) {
|
||||
// sanity check
|
||||
if (NPCs.find(id) == NPCs.end()) {
|
||||
std::cout << "npc not found: " << id << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
BaseNPC* entity = NPCs[id];
|
||||
|
||||
// sanity check
|
||||
if (!Chunking::chunkExists(entity->chunkPos)) {
|
||||
std::cout << "chunk not found!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// remove NPC from the chunk
|
||||
EntityRef ref = {id};
|
||||
Chunking::untrackEntity(entity->chunkPos, ref);
|
||||
|
||||
// remove from viewable chunks
|
||||
Chunking::removeEntityFromChunks(Chunking::getViewableChunks(entity->chunkPos), ref);
|
||||
|
||||
// finally, remove it from the map and free it
|
||||
NPCs.erase(id);
|
||||
delete entity;
|
||||
}
|
||||
|
||||
void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I, int angle) {
|
||||
BaseNPC* npc = NPCs[id];
|
||||
npc->appearanceData.iAngle = angle;
|
||||
ChunkPos oldChunk = npc->chunkPos;
|
||||
ChunkPos newChunk = Chunking::chunkPosAt(X, Y, I);
|
||||
npc->x = X;
|
||||
npc->y = Y;
|
||||
npc->z = Z;
|
||||
npc->instanceID = I;
|
||||
if (oldChunk == newChunk)
|
||||
return; // didn't change chunks
|
||||
Chunking::updateEntityChunk({id}, oldChunk, newChunk);
|
||||
}
|
||||
|
||||
void NPCManager::sendToViewable(BaseNPC *npc, void *buf, uint32_t type, size_t size) {
|
||||
for (auto it = npc->viewableChunks.begin(); it != npc->viewableChunks.end(); it++) {
|
||||
Chunk* chunk = *it;
|
||||
for (const EntityRef& ref : chunk->entities) {
|
||||
if (ref.type == EntityType::PLAYER)
|
||||
ref.sock->sendPacket(buf, type, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void npcBarkHandler(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf;
|
||||
|
||||
// get bark IDs from task data
|
||||
TaskData* td = Missions::Tasks[req->iMissionTaskID];
|
||||
std::vector<int> barks;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (td->task["m_iHBarkerTextID"][i] != 0) // non-zeroes only
|
||||
barks.push_back(td->task["m_iHBarkerTextID"][i]);
|
||||
}
|
||||
|
||||
if (barks.empty())
|
||||
return; // no barks
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_BARKER, resp);
|
||||
resp.iNPC_ID = req->iNPC_ID;
|
||||
resp.iMissionStringID = barks[Rand::rand(barks.size())];
|
||||
sock->sendPacket(resp, P_FE2CL_REP_BARKER);
|
||||
}
|
||||
|
||||
static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (plr->accountLevel > 30)
|
||||
return;
|
||||
|
||||
sP_CL2FE_REQ_NPC_UNSUMMON* req = (sP_CL2FE_REQ_NPC_UNSUMMON*)data->buf;
|
||||
NPCManager::destroyNPC(req->iNPC_ID);
|
||||
}
|
||||
|
||||
// type must already be checked and updateNPCPosition() must be called on the result
|
||||
BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) {
|
||||
uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
|
||||
#define EXTRA_HEIGHT 0
|
||||
|
||||
//assert(nextId < INT32_MAX);
|
||||
int id = nextId--;
|
||||
int team = NPCData[type]["m_iTeam"];
|
||||
BaseNPC *npc = nullptr;
|
||||
|
||||
if (team == 2) {
|
||||
npc = new Mob(x, y, z + EXTRA_HEIGHT, inst, type, NPCData[type], id);
|
||||
|
||||
// re-enable respawning, if desired
|
||||
((Mob*)npc)->summoned = !respawn;
|
||||
} else
|
||||
npc = new BaseNPC(x, y, z + EXTRA_HEIGHT, 0, inst, type, id);
|
||||
|
||||
NPCs[id] = npc;
|
||||
|
||||
return npc;
|
||||
}
|
||||
|
||||
static void npcSummonHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_NPC_SUMMON*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int limit = NPCData.back()["m_iNpcNumber"];
|
||||
|
||||
// permission & sanity check
|
||||
if (plr->accountLevel > 30 || req->iNPCType > limit || req->iNPCCnt > 100)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < req->iNPCCnt; i++) {
|
||||
BaseNPC *npc = summonNPC(plr->x, plr->y, plr->z, plr->instanceID, req->iNPCType);
|
||||
updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void handleWarp(CNSocket* sock, int32_t warpId) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
// sanity check
|
||||
if (Warps.find(warpId) == Warps.end())
|
||||
return;
|
||||
|
||||
if (plr->iPCState & 8) {
|
||||
// remove the player's vehicle
|
||||
plr->iPCState &= ~8;
|
||||
|
||||
// send to self
|
||||
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, off);
|
||||
sock->sendPacket(off, P_FE2CL_PC_VEHICLE_OFF_SUCC);
|
||||
|
||||
// send to others
|
||||
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, chg);
|
||||
chg.iPC_ID = plr->iID;
|
||||
chg.iState = plr->iPCState;
|
||||
PlayerManager::sendToViewable(sock, chg, P_FE2CL_PC_STATE_CHANGE);
|
||||
}
|
||||
|
||||
// std::cerr << "Warped to Map Num:" << Warps[warpId].instanceID << " NPC ID " << Warps[warpId].npcID << std::endl;
|
||||
if (Warps[warpId].isInstance) {
|
||||
uint64_t instanceID = Warps[warpId].instanceID;
|
||||
|
||||
// if warp requires you to be on a mission, it's gotta be a unique instance
|
||||
if (Warps[warpId].limitTaskID != 0 || instanceID == 14) { // 14 is a special case for the Time Lab
|
||||
instanceID += ((uint64_t)plr->iIDGroup << 32); // upper 32 bits are leader ID
|
||||
Chunking::createInstance(instanceID);
|
||||
|
||||
// save Lair entrance coords as a pseudo-Resurrect 'Em
|
||||
plr->recallX = Warps[warpId].x;
|
||||
plr->recallY = Warps[warpId].y;
|
||||
plr->recallZ = Warps[warpId].z + RESURRECT_HEIGHT;
|
||||
plr->recallInstance = instanceID;
|
||||
}
|
||||
|
||||
if (plr->iID == plr->iIDGroup && plr->groupCnt == 1)
|
||||
PlayerManager::sendPlayerTo(sock, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
|
||||
else {
|
||||
Player* leaderPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
||||
|
||||
for (int i = 0; i < leaderPlr->groupCnt; i++) {
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(leaderPlr->groupIDs[i]);
|
||||
CNSocket* sockTo = PlayerManager::getSockFromID(leaderPlr->groupIDs[i]);
|
||||
|
||||
if (otherPlr == nullptr || sockTo == nullptr)
|
||||
continue;
|
||||
|
||||
// save Lair entrance coords for everyone else as well
|
||||
otherPlr->recallX = Warps[warpId].x;
|
||||
otherPlr->recallY = Warps[warpId].y;
|
||||
otherPlr->recallZ = Warps[warpId].z + RESURRECT_HEIGHT;
|
||||
otherPlr->recallInstance = instanceID;
|
||||
|
||||
// remove their vehicle if they're on one
|
||||
if (otherPlr->iPCState & 8) {
|
||||
// remove the player's vehicle
|
||||
otherPlr->iPCState &= ~8;
|
||||
|
||||
// send to self
|
||||
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, off);
|
||||
sockTo->sendPacket(off, P_FE2CL_PC_VEHICLE_OFF_SUCC);
|
||||
|
||||
// send to others
|
||||
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, chg);
|
||||
chg.iPC_ID = otherPlr->iID;
|
||||
chg.iState = otherPlr->iPCState;
|
||||
PlayerManager::sendToViewable(sockTo, chg, P_FE2CL_PC_STATE_CHANGE);
|
||||
}
|
||||
|
||||
PlayerManager::sendPlayerTo(sockTo, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_NPC_SUCC, resp); // Can only be used for exiting instances because it sets the instance flag to false
|
||||
resp.iX = Warps[warpId].x;
|
||||
resp.iY = Warps[warpId].y;
|
||||
resp.iZ = Warps[warpId].z;
|
||||
resp.iCandy = plr->money;
|
||||
resp.eIL = 4; // do not take away any items
|
||||
uint64_t fromInstance = plr->instanceID; // pre-warp instance, saved for post-warp
|
||||
plr->instanceID = INSTANCE_OVERWORLD;
|
||||
Missions::failInstancedMissions(sock); // fail any instanced missions
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC);
|
||||
|
||||
PlayerManager::updatePlayerPositionForWarp(sock, resp.iX, resp.iY, resp.iZ, INSTANCE_OVERWORLD);
|
||||
|
||||
// remove the player's ongoing race, if any
|
||||
if (Racing::EPRaces.find(sock) != Racing::EPRaces.end())
|
||||
Racing::EPRaces.erase(sock);
|
||||
|
||||
// post-warp: check if the source instance has no more players in it and delete it if so
|
||||
Chunking::destroyInstanceIfEmpty(fromInstance);
|
||||
}
|
||||
}
|
||||
|
||||
static void npcWarpHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto warpNpc = (sP_CL2FE_REQ_PC_WARP_USE_NPC*)data->buf;
|
||||
handleWarp(sock, warpNpc->iWarpID);
|
||||
}
|
||||
|
||||
static void npcWarpTimeMachine(CNSocket* sock, CNPacketData* data) {
|
||||
// this is just a warp request
|
||||
handleWarp(sock, 28);
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function to get NPC closest to coordinates in specified chunks
|
||||
*/
|
||||
BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z) {
|
||||
BaseNPC* npc = nullptr;
|
||||
int lastDist = INT_MAX;
|
||||
for (auto c = chunks->begin(); c != chunks->end(); c++) { // haha get it
|
||||
Chunk* chunk = *c;
|
||||
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
|
||||
if (ent->type == EntityType::PLAYER)
|
||||
continue;
|
||||
|
||||
BaseNPC* npcTemp = (BaseNPC*)ent->getEntity();
|
||||
int distXY = std::hypot(X - npcTemp->x, Y - npcTemp->y);
|
||||
int dist = std::hypot(distXY, Z - npcTemp->z);
|
||||
if (dist < lastDist) {
|
||||
npc = npcTemp;
|
||||
lastDist = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
return npc;
|
||||
}
|
||||
|
||||
// TODO: Move this to MobAI, possibly
|
||||
#pragma region NPCEvents
|
||||
|
||||
// summon right arm and stage 2 body
|
||||
static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) {
|
||||
Mob *oldbody = (Mob*)npc; // adaptium, stun
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
std::cout << "Lord Fuse stage two" << std::endl;
|
||||
|
||||
// Fuse doesn't move; spawnX, etc. is shorter to write than *appearanceData*
|
||||
// Blastons, Heal
|
||||
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2467);
|
||||
|
||||
newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
|
||||
NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
|
||||
plr->instanceID, oldbody->appearanceData.iAngle);
|
||||
|
||||
// right arm, Adaptium, Stun
|
||||
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX - 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2469);
|
||||
|
||||
arm->appearanceData.iAngle = oldbody->appearanceData.iAngle;
|
||||
NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ,
|
||||
plr->instanceID, oldbody->appearanceData.iAngle);
|
||||
}
|
||||
|
||||
// summon left arm and stage 3 body
|
||||
static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
|
||||
Mob *oldbody = (Mob*)npc;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
std::cout << "Lord Fuse stage three" << std::endl;
|
||||
|
||||
// Cosmix, Damage Point
|
||||
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2468);
|
||||
|
||||
newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
|
||||
NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
|
||||
plr->instanceID, oldbody->appearanceData.iAngle);
|
||||
|
||||
// Blastons, Heal
|
||||
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX + 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2470);
|
||||
|
||||
arm->appearanceData.iAngle = oldbody->appearanceData.iAngle;
|
||||
NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ,
|
||||
plr->instanceID, oldbody->appearanceData.iAngle);
|
||||
}
|
||||
|
||||
std::vector<NPCEvent> NPCManager::NPCEvents = {
|
||||
NPCEvent(2466, ON_KILLED, lordFuseStageTwo),
|
||||
NPCEvent(2467, ON_KILLED, lordFuseStageThree),
|
||||
};
|
||||
|
||||
#pragma endregion NPCEvents
|
||||
|
||||
void NPCManager::queueNPCRemoval(int32_t id) {
|
||||
RemovalQueue.push(id);
|
||||
}
|
||||
|
||||
static void step(CNServer *serv, time_t currTime) {
|
||||
for (auto& pair : NPCs) {
|
||||
if (pair.second->type != EntityType::COMBAT_NPC && pair.second->type != EntityType::MOB)
|
||||
continue;
|
||||
auto npc = (CombatNPC*)pair.second;
|
||||
|
||||
npc->stepAI(currTime);
|
||||
}
|
||||
|
||||
// deallocate all NPCs queued for removal
|
||||
while (RemovalQueue.size() > 0) {
|
||||
NPCManager::destroyNPC(RemovalQueue.front());
|
||||
RemovalQueue.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void NPCManager::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_USE_NPC, npcWarpHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TIME_TO_GO_WARP, npcWarpTimeMachine);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_SUMMON, npcSummonHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler);
|
||||
|
||||
REGISTER_SHARD_TIMER(step, 200);
|
||||
}
|
||||
52
src/NPCManager.hpp
Normal file
52
src/NPCManager.hpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "NPC.hpp"
|
||||
#include "Transport.hpp"
|
||||
|
||||
#include "JSON.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#define RESURRECT_HEIGHT 400
|
||||
|
||||
enum Trigger {
|
||||
ON_KILLED,
|
||||
ON_COMBAT
|
||||
};
|
||||
|
||||
typedef void (*NPCEventHandler)(CNSocket*, BaseNPC*);
|
||||
|
||||
struct NPCEvent {
|
||||
int32_t npcType;
|
||||
int trigger;
|
||||
NPCEventHandler handler;
|
||||
|
||||
NPCEvent(int32_t t, int tr, NPCEventHandler hndlr)
|
||||
: npcType(t), trigger(tr), handler(hndlr) {}
|
||||
};
|
||||
|
||||
struct WarpLocation;
|
||||
|
||||
namespace NPCManager {
|
||||
extern std::unordered_map<int32_t, BaseNPC*> NPCs;
|
||||
extern std::map<int32_t, WarpLocation> Warps;
|
||||
extern std::vector<WarpLocation> RespawnPoints;
|
||||
extern std::vector<NPCEvent> NPCEvents;
|
||||
extern nlohmann::json NPCData;
|
||||
extern int32_t nextId;
|
||||
void init();
|
||||
|
||||
void queueNPCRemoval(int32_t);
|
||||
void destroyNPC(int32_t);
|
||||
void updateNPCPosition(int32_t, int X, int Y, int Z, uint64_t I, int angle);
|
||||
|
||||
void sendToViewable(BaseNPC* npc, void* buf, uint32_t type, size_t size);
|
||||
|
||||
BaseNPC *summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn=false, bool baseInstance=false);
|
||||
|
||||
BaseNPC* getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z);
|
||||
}
|
||||
406
src/Nanos.cpp
Normal file
406
src/Nanos.cpp
Normal file
@@ -0,0 +1,406 @@
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
#include "Combat.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Groups.hpp"
|
||||
#include "Abilities.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using namespace Nanos;
|
||||
|
||||
std::map<int32_t, NanoData> Nanos::NanoTable;
|
||||
std::map<int32_t, NanoTuning> Nanos::NanoTunings;
|
||||
|
||||
#pragma region Helper methods
|
||||
void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm) {
|
||||
if (nanoID <= 0 || nanoID >= NANO_COUNT)
|
||||
return;
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int level = plr->level;
|
||||
|
||||
#ifndef ACADEMY
|
||||
level = nanoID < plr->level ? plr->level : nanoID;
|
||||
|
||||
/*
|
||||
* Spend the necessary Fusion Matter.
|
||||
* Note the use of the not-yet-incremented plr->level as opposed to level.
|
||||
* Doing it the other way always leaves the FM at 0. Jade totally called it.
|
||||
*/
|
||||
plr->level = level;
|
||||
|
||||
if (spendfm)
|
||||
Missions::updateFusionMatter(sock, -(int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]);
|
||||
#endif
|
||||
|
||||
// Send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_NANO_CREATE_SUCC, resp);
|
||||
resp.Nano.iID = nanoID;
|
||||
resp.Nano.iStamina = 150;
|
||||
resp.iQuestItemSlotNum = slot;
|
||||
resp.iPC_Level = level;
|
||||
resp.iPC_FusionMatter = plr->fusionmatter;
|
||||
|
||||
if (plr->activeNano > 0 && plr->activeNano == nanoID)
|
||||
summonNano(sock, -1); // just unsummon the nano to prevent infinite buffs
|
||||
|
||||
// Update player
|
||||
plr->Nanos[nanoID] = resp.Nano;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_NANO_CREATE_SUCC);
|
||||
|
||||
/*
|
||||
* iPC_Level in NANO_CREATE_SUCC sets the player's level.
|
||||
* Other players must be notified of the change as well. Both P_FE2CL_REP_PC_NANO_CREATE and
|
||||
* P_FE2CL_REP_PC_CHANGE_LEVEL appear to play the same animation, but only the latter affects
|
||||
* the other player's displayed level.
|
||||
*/
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL, resp2);
|
||||
|
||||
resp2.iPC_ID = plr->iID;
|
||||
resp2.iPC_Level = level;
|
||||
|
||||
// Update other players' perception of the player's level
|
||||
PlayerManager::sendToViewable(sock, resp2, P_FE2CL_REP_PC_CHANGE_LEVEL);
|
||||
}
|
||||
|
||||
void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
|
||||
INITSTRUCT(sP_FE2CL_REP_NANO_ACTIVE_SUCC, resp);
|
||||
resp.iActiveNanoSlotNum = slot;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (slot > 2 || slot < -1)
|
||||
return; // sanity check
|
||||
|
||||
int16_t nanoID = slot == -1 ? 0 : plr->equippedNanos[slot];
|
||||
|
||||
if (slot != -1 && plr->Nanos[nanoID].iSkillID == 0)
|
||||
return; // prevent powerless nanos from summoning
|
||||
|
||||
plr->nanoDrainRate = 0;
|
||||
int16_t skillID = plr->Nanos[plr->activeNano].iSkillID;
|
||||
|
||||
// passive nano unbuffing
|
||||
if (SkillTable[skillID].drainType == 2) {
|
||||
std::vector<int> targetData = findTargets(plr, skillID);
|
||||
|
||||
for (auto& pwr : NanoPowers)
|
||||
if (pwr.skillType == SkillTable[skillID].skillType)
|
||||
nanoUnbuff(sock, targetData, pwr.bitFlag, pwr.timeBuffID, 0,(SkillTable[skillID].targetType == 3));
|
||||
}
|
||||
|
||||
if (nanoID >= NANO_COUNT || nanoID < 0)
|
||||
return; // sanity check
|
||||
|
||||
plr->activeNano = nanoID;
|
||||
skillID = plr->Nanos[nanoID].iSkillID;
|
||||
|
||||
// passive nano buffing
|
||||
if (SkillTable[skillID].drainType == 2) {
|
||||
std::vector<int> targetData = findTargets(plr, skillID);
|
||||
|
||||
int boost = 0;
|
||||
if (getNanoBoost(plr))
|
||||
boost = 1;
|
||||
|
||||
for (auto& pwr : NanoPowers) {
|
||||
if (pwr.skillType == SkillTable[skillID].skillType) {
|
||||
resp.eCSTB___Add = 1; // the part that makes nano go ZOOMAZOOM
|
||||
plr->nanoDrainRate = SkillTable[skillID].batteryUse[boost*3];
|
||||
|
||||
pwr.handle(sock, targetData, nanoID, skillID, 0, SkillTable[skillID].powerIntensity[boost]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!silent) // silent nano death but only for the summoning player
|
||||
sock->sendPacket(resp, P_FE2CL_REP_NANO_ACTIVE_SUCC);
|
||||
|
||||
// Send to other players, these players can't handle silent nano deaths so this packet needs to be sent.
|
||||
INITSTRUCT(sP_FE2CL_NANO_ACTIVE, pkt1);
|
||||
pkt1.iPC_ID = plr->iID;
|
||||
pkt1.Nano = plr->Nanos[nanoID];
|
||||
PlayerManager::sendToViewable(sock, pkt1, P_FE2CL_NANO_ACTIVE);
|
||||
}
|
||||
|
||||
static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
|
||||
if (skill->iNanoID >= NANO_COUNT)
|
||||
return;
|
||||
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (plr->activeNano > 0 && plr->activeNano == skill->iNanoID)
|
||||
summonNano(sock, -1); // just unsummon the nano to prevent infinite buffs
|
||||
|
||||
sNano nano = plr->Nanos[skill->iNanoID];
|
||||
nano.iSkillID = skill->iTuneID;
|
||||
plr->Nanos[skill->iNanoID] = nano;
|
||||
|
||||
// Send to client
|
||||
INITSTRUCT(sP_FE2CL_REP_NANO_TUNE_SUCC, resp);
|
||||
resp.iNanoID = skill->iNanoID;
|
||||
resp.iSkillID = skill->iTuneID;
|
||||
resp.iPC_FusionMatter = plr->fusionmatter;
|
||||
resp.aItem[9] = plr->Inven[0]; // quick fix to make sure item in slot 0 doesn't get yeeted by default
|
||||
|
||||
|
||||
// check if there's any garbage in the item slot array (this'll happen when a nano station isn't used)
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (skill->aiNeedItemSlotNum[i] < 0 || skill->aiNeedItemSlotNum[i] >= AINVEN_COUNT) {
|
||||
sock->sendPacket(resp, P_FE2CL_REP_NANO_TUNE_SUCC);
|
||||
return; // stop execution, don't run consumption logic
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef ACADEMY
|
||||
if (plr->fusionmatter < (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"]) // sanity check
|
||||
return;
|
||||
#endif
|
||||
|
||||
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"];
|
||||
|
||||
int reqItemCount = NanoTunings[skill->iTuneID].reqItemCount;
|
||||
int reqItemID = NanoTunings[skill->iTuneID].reqItems;
|
||||
int i = 0;
|
||||
while (reqItemCount > 0 && i < 10) {
|
||||
|
||||
sItemBase& item = plr->Inven[skill->aiNeedItemSlotNum[i]];
|
||||
if (item.iType == 7 && item.iID == reqItemID) {
|
||||
if (item.iOpt > reqItemCount) {
|
||||
item.iOpt -= reqItemCount;
|
||||
reqItemCount = 0;
|
||||
}
|
||||
else {
|
||||
reqItemCount -= item.iOpt;
|
||||
item.iID = 0;
|
||||
item.iType = 0;
|
||||
item.iOpt = 0;
|
||||
}
|
||||
}
|
||||
i++; // next slot
|
||||
}
|
||||
|
||||
resp.iPC_FusionMatter = plr->fusionmatter; // update fusion matter in packet
|
||||
// update items clientside
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (skill->aiNeedItemSlotNum[i]) { // non-zero check
|
||||
resp.aItem[i] = plr->Inven[skill->aiNeedItemSlotNum[i]];
|
||||
resp.aiItemSlotNum[i] = skill->aiNeedItemSlotNum[i];
|
||||
}
|
||||
}
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_NANO_TUNE_SUCC);
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << PlayerManager::getPlayerName(plr) << " set skill id " << skill->iTuneID << " for nano: " << skill->iNanoID << std::endl;
|
||||
)
|
||||
}
|
||||
|
||||
// 0=A 1=B 2=C -1=Not found
|
||||
int Nanos::nanoStyle(int nanoID) {
|
||||
if (nanoID < 1 || nanoID >= (int)NanoTable.size())
|
||||
return -1;
|
||||
return NanoTable[nanoID].style;
|
||||
}
|
||||
|
||||
bool Nanos::getNanoBoost(Player* plr) {
|
||||
for (int i = 0; i < 3; i++)
|
||||
if (plr->equippedNanos[i] == plr->activeNano)
|
||||
if (plr->iConditionBitFlag & (CSB_BIT_STIMPAKSLOT1 << i))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
static void nanoEquipHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto nano = (sP_CL2FE_REQ_NANO_EQUIP*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_NANO_EQUIP_SUCC, resp);
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// sanity checks
|
||||
if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0)
|
||||
return;
|
||||
|
||||
if (nano->iNanoID < 0 || nano->iNanoID >= NANO_COUNT)
|
||||
return;
|
||||
|
||||
resp.iNanoID = nano->iNanoID;
|
||||
resp.iNanoSlotNum = nano->iNanoSlotNum;
|
||||
|
||||
// Update player
|
||||
plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID;
|
||||
|
||||
// Unbuff gumballs
|
||||
int value1 = CSB_BIT_STIMPAKSLOT1 << nano->iNanoSlotNum;
|
||||
if (plr->iConditionBitFlag & value1) {
|
||||
int value2 = ECSB_STIMPAKSLOT1 + nano->iNanoSlotNum;
|
||||
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
|
||||
pkt.eCSTB = value2; // eCharStatusTimeBuffID
|
||||
pkt.eTBU = 2; // eTimeBuffUpdate
|
||||
pkt.eTBT = 1; // eTimeBuffType 1 means nano
|
||||
pkt.iConditionBitFlag = plr->iConditionBitFlag &= ~value1;
|
||||
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
|
||||
}
|
||||
|
||||
// unsummon nano if replaced
|
||||
if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum])
|
||||
summonNano(sock, -1);
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_NANO_EQUIP_SUCC);
|
||||
}
|
||||
|
||||
static void nanoUnEquipHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto nano = (sP_CL2FE_REQ_NANO_UNEQUIP*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_NANO_UNEQUIP_SUCC, resp);
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// sanity check
|
||||
if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0)
|
||||
return;
|
||||
|
||||
resp.iNanoSlotNum = nano->iNanoSlotNum;
|
||||
|
||||
// unsummon nano if removed
|
||||
if (plr->equippedNanos[nano->iNanoSlotNum] == plr->activeNano)
|
||||
summonNano(sock, -1);
|
||||
|
||||
// update player
|
||||
plr->equippedNanos[nano->iNanoSlotNum] = 0;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_NANO_UNEQUIP_SUCC);
|
||||
}
|
||||
|
||||
static void nanoSummonHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto pkt = (sP_CL2FE_REQ_NANO_ACTIVE*)data->buf;
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
summonNano(sock, pkt->iNanoSlotNum);
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano slot: " << pkt->iNanoSlotNum << std::endl;
|
||||
)
|
||||
}
|
||||
|
||||
static void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int16_t nanoID = plr->activeNano;
|
||||
int16_t skillID = plr->Nanos[nanoID].iSkillID;
|
||||
|
||||
DEBUGLOG(
|
||||
std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano skill " << std::endl;
|
||||
)
|
||||
|
||||
std::vector<int> targetData = findTargets(plr, skillID, data);
|
||||
|
||||
int boost = 0;
|
||||
if (getNanoBoost(plr))
|
||||
boost = 1;
|
||||
|
||||
plr->Nanos[plr->activeNano].iStamina -= SkillTable[skillID].batteryUse[boost*3];
|
||||
if (plr->Nanos[plr->activeNano].iStamina < 0)
|
||||
plr->Nanos[plr->activeNano].iStamina = 0;
|
||||
|
||||
for (auto& pwr : NanoPowers)
|
||||
if (pwr.skillType == SkillTable[skillID].skillType)
|
||||
pwr.handle(sock, targetData, nanoID, skillID, SkillTable[skillID].durationTime[boost], SkillTable[skillID].powerIntensity[boost]);
|
||||
|
||||
if (plr->Nanos[plr->activeNano].iStamina < 0)
|
||||
summonNano(sock, -1);
|
||||
}
|
||||
|
||||
static void nanoSkillSetHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto skill = (sP_CL2FE_REQ_NANO_TUNE*)data->buf;
|
||||
setNanoSkill(sock, skill);
|
||||
}
|
||||
|
||||
static void nanoSkillSetGMHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto skillGM = (sP_CL2FE_REQ_NANO_TUNE*)data->buf;
|
||||
setNanoSkill(sock, skillGM);
|
||||
}
|
||||
|
||||
static void nanoRecallRegisterHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto recallData = (sP_CL2FE_REQ_REGIST_RXCOM*)data->buf;
|
||||
|
||||
if (NPCManager::NPCs.find(recallData->iNPCID) == NPCManager::NPCs.end())
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
BaseNPC *npc = NPCManager::NPCs[recallData->iNPCID];
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_REGIST_RXCOM, response);
|
||||
response.iMapNum = plr->recallInstance = (int32_t)npc->instanceID; // Never going to recall into a Fusion Lair
|
||||
response.iX = plr->recallX = npc->x;
|
||||
response.iY = plr->recallY = npc->y;
|
||||
response.iZ = plr->recallZ = npc->z;
|
||||
sock->sendPacket(response, P_FE2CL_REP_REGIST_RXCOM);
|
||||
}
|
||||
|
||||
static void nanoRecallHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto recallData = (sP_CL2FE_REQ_WARP_USE_RECALL*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* otherPlr = PlayerManager::getPlayerFromID(recallData->iGroupMemberID);
|
||||
if (otherPlr == nullptr)
|
||||
return;
|
||||
|
||||
// ensure the group member is still in the same IZ
|
||||
if (otherPlr->instanceID != plr->instanceID)
|
||||
return;
|
||||
|
||||
// do not allow hypothetical recall points in lairs to mess with the respawn logic
|
||||
if (PLAYERID(plr->instanceID) != 0)
|
||||
return;
|
||||
|
||||
if ((int32_t)plr->instanceID == otherPlr->recallInstance)
|
||||
PlayerManager::sendPlayerTo(sock, otherPlr->recallX, otherPlr->recallY, otherPlr->recallZ, otherPlr->recallInstance);
|
||||
else {
|
||||
INITSTRUCT(sP_FE2CL_REP_WARP_USE_RECALL_FAIL, response)
|
||||
sock->sendPacket(response, P_FE2CL_REP_WARP_USE_RECALL_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
static void nanoPotionHandler(CNSocket* sock, CNPacketData* data) {
|
||||
Player* player = PlayerManager::getPlayer(sock);
|
||||
|
||||
// sanity checks
|
||||
if (player->activeNano == -1 || player->batteryN == 0)
|
||||
return;
|
||||
|
||||
sNano nano = player->Nanos[player->activeNano];
|
||||
int difference = 150 - nano.iStamina;
|
||||
if (player->batteryN < difference)
|
||||
difference = player->batteryN;
|
||||
|
||||
if (difference == 0)
|
||||
return;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_CHARGE_NANO_STAMINA, response);
|
||||
response.iNanoID = nano.iID;
|
||||
response.iNanoStamina = nano.iStamina + difference;
|
||||
response.iBatteryN = player->batteryN - difference;
|
||||
|
||||
sock->sendPacket(response, P_FE2CL_REP_CHARGE_NANO_STAMINA);
|
||||
// now update serverside
|
||||
player->batteryN -= difference;
|
||||
player->Nanos[nano.iID].iStamina += difference;
|
||||
|
||||
}
|
||||
|
||||
void Nanos::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_ACTIVE, nanoSummonHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_EQUIP, nanoEquipHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_UNEQUIP, nanoUnEquipHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_TUNE, nanoSkillSetHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_NANO_SKILL, nanoSkillSetGMHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_SKILL_USE, nanoSkillUseHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REGIST_RXCOM, nanoRecallRegisterHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_WARP_USE_RECALL, nanoRecallHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_CHARGE_NANO_STAMINA, nanoPotionHandler);
|
||||
}
|
||||
28
src/Nanos.hpp
Normal file
28
src/Nanos.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
struct NanoData {
|
||||
int style;
|
||||
};
|
||||
|
||||
struct NanoTuning {
|
||||
int reqItemCount;
|
||||
int reqItems;
|
||||
};
|
||||
|
||||
namespace Nanos {
|
||||
extern std::map<int32_t, NanoData> NanoTable;
|
||||
extern std::map<int32_t, NanoTuning> NanoTunings;
|
||||
void init();
|
||||
|
||||
// Helper methods
|
||||
void addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm=false);
|
||||
void summonNano(CNSocket* sock, int slot, bool silent = false);
|
||||
int nanoStyle(int nanoID);
|
||||
bool getNanoBoost(Player* plr);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
#include "Player.hpp"
|
||||
101
src/Player.hpp
101
src/Player.hpp
@@ -1,25 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
|
||||
#ifndef _PLR_HPP
|
||||
#define _PLR_HPP
|
||||
#include "core/Core.hpp"
|
||||
#include "Chunking.hpp"
|
||||
#include "Entities.hpp"
|
||||
|
||||
#include "CNProtocol.hpp"
|
||||
#include "CNStructs.hpp"
|
||||
#define ACTIVE_MISSION_COUNT 6
|
||||
|
||||
struct Player {
|
||||
int64_t SerialKey;
|
||||
int32_t iID;
|
||||
uint64_t FEKey;
|
||||
#define PC_MAXHEALTH(level) (925 + 75 * (level))
|
||||
|
||||
int level;
|
||||
int HP;
|
||||
int slot;
|
||||
sPCStyle PCStyle;
|
||||
sPCStyle2 PCStyle2;
|
||||
struct Player : public Entity {
|
||||
int accountId = 0;
|
||||
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
|
||||
int32_t iID = 0;
|
||||
|
||||
int x, y, z, angle;
|
||||
sItemBase Equip[AEQUIP_COUNT];
|
||||
int level = 0;
|
||||
int HP = 0;
|
||||
int slot = 0; // player slot, not nano slot
|
||||
int16_t mentor = 0;
|
||||
int32_t money = 0;
|
||||
int32_t fusionmatter = 0;
|
||||
int32_t batteryW = 0;
|
||||
int32_t batteryN = 0;
|
||||
sPCStyle PCStyle = {};
|
||||
sPCStyle2 PCStyle2 = {};
|
||||
sNano Nanos[NANO_COUNT] = {}; // acquired nanos
|
||||
int equippedNanos[3] = {};
|
||||
int activeNano = 0; // active nano (index into Nanos)
|
||||
int8_t iPCState = 0;
|
||||
int32_t iWarpLocationFlag = 0;
|
||||
int64_t aSkywayLocationFlag[2] = {};
|
||||
int32_t iConditionBitFlag = 0;
|
||||
int32_t iSelfConditionBitFlag = 0;
|
||||
int8_t iSpecialState = 0;
|
||||
|
||||
int angle = 0;
|
||||
int lastX = 0, lastY = 0, lastZ = 0, lastAngle = 0;
|
||||
int recallX = 0, recallY = 0, recallZ = 0, recallInstance = 0; // also Lair entrances
|
||||
sItemBase Equip[AEQUIP_COUNT] = {};
|
||||
sItemBase Inven[AINVEN_COUNT] = {};
|
||||
sItemBase Bank[ABANK_COUNT] = {};
|
||||
sItemTrade Trade[12] = {};
|
||||
int32_t moneyInTrade = 0;
|
||||
bool isTrading = false;
|
||||
bool isTradeConfirm = false;
|
||||
|
||||
bool inCombat = false;
|
||||
bool onMonkey = false;
|
||||
int nanoDrainRate = 0;
|
||||
int healCooldown = 0;
|
||||
|
||||
int pointDamage = 0;
|
||||
int groupDamage = 0;
|
||||
int fireRate = 0;
|
||||
int defense = 0;
|
||||
|
||||
int64_t aQuestFlag[16] = {};
|
||||
int tasks[ACTIVE_MISSION_COUNT] = {};
|
||||
int RemainingNPCCount[ACTIVE_MISSION_COUNT][3] = {};
|
||||
sItemBase QInven[AQINVEN_COUNT] = {};
|
||||
int32_t CurrentMissionID = 0;
|
||||
|
||||
sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {};
|
||||
|
||||
int32_t iIDGroup = 0;
|
||||
int groupCnt = 0;
|
||||
int32_t groupIDs[4] = {};
|
||||
int32_t iGroupConditionBitFlag = 0;
|
||||
|
||||
bool notify = false;
|
||||
bool hidden = false;
|
||||
bool unwarpable = false;
|
||||
|
||||
bool buddiesSynced = false;
|
||||
int64_t buddyIDs[50] = {};
|
||||
bool isBuddyBlocked[50] = {};
|
||||
|
||||
uint64_t iFirstUseFlag[2] = {};
|
||||
time_t lastHeartbeat = 0;
|
||||
|
||||
int suspicionRating = 0;
|
||||
time_t lastShot = 0;
|
||||
std::vector<sItemBase> buyback = {};
|
||||
|
||||
Player() { type = EntityType::PLAYER; }
|
||||
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,53 @@
|
||||
#ifndef _PM_HPP
|
||||
#define _PM_HPP
|
||||
#pragma once
|
||||
|
||||
#include "Player.hpp"
|
||||
#include "CNProtocol.hpp"
|
||||
#include "CNStructs.hpp"
|
||||
#include "CNShardServer.hpp"
|
||||
#include "core/Core.hpp"
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "Chunking.hpp"
|
||||
|
||||
#include <utility>
|
||||
#include <map>
|
||||
#include <list>
|
||||
|
||||
struct PlayerView {
|
||||
std::list<CNSocket*> viewable;
|
||||
Player plr;
|
||||
};
|
||||
|
||||
struct WarpLocation;
|
||||
|
||||
namespace PlayerManager {
|
||||
extern std::map<CNSocket*, PlayerView> players;
|
||||
extern std::map<CNSocket*, Player*> players;
|
||||
void init();
|
||||
|
||||
void addPlayer(CNSocket* key, Player plr);
|
||||
void removePlayer(CNSocket* key);
|
||||
Player getPlayer(CNSocket* key);
|
||||
|
||||
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z);
|
||||
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle);
|
||||
void updatePlayerPositionForWarp(CNSocket* sock, int X, int Y, int Z, uint64_t inst);
|
||||
|
||||
void enterPlayer(CNSocket* sock, CNPacketData* data);
|
||||
void loadPlayer(CNSocket* sock, CNPacketData* data);
|
||||
void movePlayer(CNSocket* sock, CNPacketData* data);
|
||||
void stopPlayer(CNSocket* sock, CNPacketData* data);
|
||||
void jumpPlayer(CNSocket* sock, CNPacketData* data);
|
||||
void movePlatformPlayer(CNSocket* sock, CNPacketData* data);
|
||||
void gotoPlayer(CNSocket* sock, CNPacketData* data);
|
||||
void setSpecialPlayer(CNSocket* sock, CNPacketData* data);
|
||||
|
||||
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I);
|
||||
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z);
|
||||
|
||||
Player *getPlayer(CNSocket* key);
|
||||
std::string getPlayerName(Player *plr, bool id=true);
|
||||
|
||||
bool isAccountInUse(int accountId);
|
||||
void exitDuplicate(int accountId);
|
||||
Player *getPlayerFromID(int32_t iID);
|
||||
CNSocket *getSockFromID(int32_t iID);
|
||||
CNSocket *getSockFromName(std::string firstname, std::string lastname);
|
||||
CNSocket *getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname);
|
||||
WarpLocation *getRespawnPoint(Player *plr);
|
||||
|
||||
void sendToViewable(CNSocket *sock, void* buf, uint32_t type, size_t size);
|
||||
|
||||
// TODO: unify this under the new Entity system
|
||||
template<class T>
|
||||
void sendToViewable(CNSocket *sock, T& pkt, uint32_t type) {
|
||||
Player* plr = getPlayer(sock);
|
||||
for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) {
|
||||
Chunk* chunk = *it;
|
||||
for (const EntityRef& ref : chunk->entities) {
|
||||
if (ref.type != EntityType::PLAYER || ref.sock == sock)
|
||||
continue;
|
||||
|
||||
ref.sock->sendPacket(pkt, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
287
src/PlayerMovement.cpp
Normal file
287
src/PlayerMovement.cpp
Normal file
@@ -0,0 +1,287 @@
|
||||
#include "PlayerMovement.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "TableData.hpp"
|
||||
#include "core/Core.hpp"
|
||||
|
||||
static void movePlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto* moveData = (sP_CL2FE_REQ_PC_MOVE*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, moveData->iX, moveData->iY, moveData->iZ, plr->instanceID, moveData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_MOVE, moveResponse);
|
||||
|
||||
moveResponse.iID = plr->iID;
|
||||
moveResponse.cKeyValue = moveData->cKeyValue;
|
||||
|
||||
moveResponse.iX = moveData->iX;
|
||||
moveResponse.iY = moveData->iY;
|
||||
moveResponse.iZ = moveData->iZ;
|
||||
moveResponse.iAngle = moveData->iAngle;
|
||||
moveResponse.fVX = moveData->fVX;
|
||||
moveResponse.fVY = moveData->fVY;
|
||||
moveResponse.fVZ = moveData->fVZ;
|
||||
|
||||
moveResponse.iSpeed = moveData->iSpeed;
|
||||
moveResponse.iCliTime = moveData->iCliTime; // maybe don't send this??? seems unneeded...
|
||||
moveResponse.iSvrTime = tm;
|
||||
|
||||
PlayerManager::sendToViewable(sock, moveResponse, P_FE2CL_PC_MOVE);
|
||||
|
||||
// [gruntwork] check if player has a follower and move it
|
||||
if (TableData::RunningNPCPaths.find(plr->iID) != TableData::RunningNPCPaths.end()) {
|
||||
BaseNPC* follower = TableData::RunningNPCPaths[plr->iID].first;
|
||||
Transport::NPCQueues.erase(follower->appearanceData.iNPC_ID); // erase existing points
|
||||
std::queue<Vec3> queue;
|
||||
Vec3 from = { follower->x, follower->y, follower->z };
|
||||
float drag = 0.95f; // this ensures that they don't bump into the player
|
||||
Vec3 to = {
|
||||
(int)(follower->x + (moveData->iX - follower->x) * drag),
|
||||
(int)(follower->y + (moveData->iY - follower->y) * drag),
|
||||
(int)(follower->z + (moveData->iZ - follower->z) * drag)
|
||||
};
|
||||
|
||||
// add a route to the queue; to be processed in Transport::stepNPCPathing()
|
||||
Transport::lerp(&queue, from, to, NPC_DEFAULT_SPEED * 1.5); // little faster than typical
|
||||
Transport::NPCQueues[follower->appearanceData.iNPC_ID] = queue;
|
||||
}
|
||||
}
|
||||
|
||||
static void stopPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto stopData = (sP_CL2FE_REQ_PC_STOP*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, stopData->iX, stopData->iY, stopData->iZ, plr->instanceID, plr->angle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_STOP, stopResponse);
|
||||
|
||||
stopResponse.iID = plr->iID;
|
||||
|
||||
stopResponse.iX = stopData->iX;
|
||||
stopResponse.iY = stopData->iY;
|
||||
stopResponse.iZ = stopData->iZ;
|
||||
|
||||
stopResponse.iCliTime = stopData->iCliTime; // maybe don't send this??? seems unneeded...
|
||||
stopResponse.iSvrTime = tm;
|
||||
|
||||
PlayerManager::sendToViewable(sock, stopResponse, P_FE2CL_PC_STOP);
|
||||
}
|
||||
|
||||
static void jumpPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto jumpData = (sP_CL2FE_REQ_PC_JUMP*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, jumpData->iX, jumpData->iY, jumpData->iZ, plr->instanceID, jumpData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_JUMP, jumpResponse);
|
||||
|
||||
jumpResponse.iID = plr->iID;
|
||||
jumpResponse.cKeyValue = jumpData->cKeyValue;
|
||||
|
||||
jumpResponse.iX = jumpData->iX;
|
||||
jumpResponse.iY = jumpData->iY;
|
||||
jumpResponse.iZ = jumpData->iZ;
|
||||
jumpResponse.iAngle = jumpData->iAngle;
|
||||
jumpResponse.iVX = jumpData->iVX;
|
||||
jumpResponse.iVY = jumpData->iVY;
|
||||
jumpResponse.iVZ = jumpData->iVZ;
|
||||
|
||||
jumpResponse.iSpeed = jumpData->iSpeed;
|
||||
jumpResponse.iCliTime = jumpData->iCliTime; // maybe don't send this??? seems unneeded...
|
||||
jumpResponse.iSvrTime = tm;
|
||||
|
||||
PlayerManager::sendToViewable(sock, jumpResponse, P_FE2CL_PC_JUMP);
|
||||
}
|
||||
|
||||
static void jumppadPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto jumppadData = (sP_CL2FE_REQ_PC_JUMPPAD*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, jumppadData->iX, jumppadData->iY, jumppadData->iZ, plr->instanceID, jumppadData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_JUMPPAD, jumppadResponse);
|
||||
|
||||
jumppadResponse.iPC_ID = plr->iID;
|
||||
jumppadResponse.cKeyValue = jumppadData->cKeyValue;
|
||||
|
||||
jumppadResponse.iX = jumppadData->iX;
|
||||
jumppadResponse.iY = jumppadData->iY;
|
||||
jumppadResponse.iZ = jumppadData->iZ;
|
||||
jumppadResponse.iVX = jumppadData->iVX;
|
||||
jumppadResponse.iVY = jumppadData->iVY;
|
||||
jumppadResponse.iVZ = jumppadData->iVZ;
|
||||
|
||||
jumppadResponse.iCliTime = jumppadData->iCliTime;
|
||||
jumppadResponse.iSvrTime = tm;
|
||||
|
||||
PlayerManager::sendToViewable(sock, jumppadResponse, P_FE2CL_PC_JUMPPAD);
|
||||
}
|
||||
|
||||
static void launchPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto launchData = (sP_CL2FE_REQ_PC_LAUNCHER*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, launchData->iX, launchData->iY, launchData->iZ, plr->instanceID, launchData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_LAUNCHER, launchResponse);
|
||||
|
||||
launchResponse.iPC_ID = plr->iID;
|
||||
|
||||
launchResponse.iX = launchData->iX;
|
||||
launchResponse.iY = launchData->iY;
|
||||
launchResponse.iZ = launchData->iZ;
|
||||
launchResponse.iVX = launchData->iVX;
|
||||
launchResponse.iVY = launchData->iVY;
|
||||
launchResponse.iVZ = launchData->iVZ;
|
||||
launchResponse.iSpeed = launchData->iSpeed;
|
||||
launchResponse.iAngle = launchData->iAngle;
|
||||
|
||||
launchResponse.iCliTime = launchData->iCliTime;
|
||||
launchResponse.iSvrTime = tm;
|
||||
|
||||
PlayerManager::sendToViewable(sock, launchResponse, P_FE2CL_PC_LAUNCHER);
|
||||
}
|
||||
|
||||
static void ziplinePlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
sP_CL2FE_REQ_PC_ZIPLINE* ziplineData = (sP_CL2FE_REQ_PC_ZIPLINE*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, ziplineData->iX, ziplineData->iY, ziplineData->iZ, plr->instanceID, ziplineData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_ZIPLINE, ziplineResponse);
|
||||
|
||||
ziplineResponse.iPC_ID = plr->iID;
|
||||
ziplineResponse.iCliTime = ziplineData->iCliTime;
|
||||
ziplineResponse.iSvrTime = tm;
|
||||
ziplineResponse.iX = ziplineData->iX;
|
||||
ziplineResponse.iY = ziplineData->iY;
|
||||
ziplineResponse.iZ = ziplineData->iZ;
|
||||
ziplineResponse.fVX = ziplineData->fVX;
|
||||
ziplineResponse.fVY = ziplineData->fVY;
|
||||
ziplineResponse.fVZ = ziplineData->fVZ;
|
||||
ziplineResponse.fMovDistance = ziplineData->fMovDistance;
|
||||
ziplineResponse.fMaxDistance = ziplineData->fMaxDistance;
|
||||
ziplineResponse.fDummy = ziplineData->fDummy; // wtf is this for?
|
||||
ziplineResponse.iStX = ziplineData->iStX;
|
||||
ziplineResponse.iStY = ziplineData->iStY;
|
||||
ziplineResponse.iStZ = ziplineData->iStZ;
|
||||
ziplineResponse.bDown = ziplineData->bDown;
|
||||
ziplineResponse.iSpeed = ziplineData->iSpeed;
|
||||
ziplineResponse.iAngle = ziplineData->iAngle;
|
||||
ziplineResponse.iRollMax = ziplineData->iRollMax;
|
||||
ziplineResponse.iRoll = ziplineData->iRoll;
|
||||
|
||||
PlayerManager::sendToViewable(sock, ziplineResponse, P_FE2CL_PC_ZIPLINE);
|
||||
}
|
||||
|
||||
static void movePlatformPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto platformData = (sP_CL2FE_REQ_PC_MOVEPLATFORM*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, platformData->iX, platformData->iY, platformData->iZ, plr->instanceID, platformData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_MOVEPLATFORM, platResponse);
|
||||
|
||||
platResponse.iPC_ID = plr->iID;
|
||||
platResponse.iCliTime = platformData->iCliTime;
|
||||
platResponse.iSvrTime = tm;
|
||||
platResponse.iX = platformData->iX;
|
||||
platResponse.iY = platformData->iY;
|
||||
platResponse.iZ = platformData->iZ;
|
||||
platResponse.iAngle = platformData->iAngle;
|
||||
platResponse.fVX = platformData->fVX;
|
||||
platResponse.fVY = platformData->fVY;
|
||||
platResponse.fVZ = platformData->fVZ;
|
||||
platResponse.iLcX = platformData->iLcX;
|
||||
platResponse.iLcY = platformData->iLcY;
|
||||
platResponse.iLcZ = platformData->iLcZ;
|
||||
platResponse.iSpeed = platformData->iSpeed;
|
||||
platResponse.bDown = platformData->bDown;
|
||||
platResponse.cKeyValue = platformData->cKeyValue;
|
||||
platResponse.iPlatformID = platformData->iPlatformID;
|
||||
|
||||
PlayerManager::sendToViewable(sock, platResponse, P_FE2CL_PC_MOVEPLATFORM);
|
||||
}
|
||||
|
||||
static void moveSliderPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
auto sliderData = (sP_CL2FE_REQ_PC_MOVETRANSPORTATION*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, sliderData->iX, sliderData->iY, sliderData->iZ, plr->instanceID, sliderData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_MOVETRANSPORTATION, sliderResponse);
|
||||
|
||||
sliderResponse.iPC_ID = plr->iID;
|
||||
sliderResponse.iCliTime = sliderData->iCliTime;
|
||||
sliderResponse.iSvrTime = tm;
|
||||
sliderResponse.iX = sliderData->iX;
|
||||
sliderResponse.iY = sliderData->iY;
|
||||
sliderResponse.iZ = sliderData->iZ;
|
||||
sliderResponse.iAngle = sliderData->iAngle;
|
||||
sliderResponse.fVX = sliderData->fVX;
|
||||
sliderResponse.fVY = sliderData->fVY;
|
||||
sliderResponse.fVZ = sliderData->fVZ;
|
||||
sliderResponse.iLcX = sliderData->iLcX;
|
||||
sliderResponse.iLcY = sliderData->iLcY;
|
||||
sliderResponse.iLcZ = sliderData->iLcZ;
|
||||
sliderResponse.iSpeed = sliderData->iSpeed;
|
||||
sliderResponse.cKeyValue = sliderData->cKeyValue;
|
||||
sliderResponse.iT_ID = sliderData->iT_ID;
|
||||
|
||||
PlayerManager::sendToViewable(sock, sliderResponse, P_FE2CL_PC_MOVETRANSPORTATION);
|
||||
}
|
||||
|
||||
static void moveSlopePlayer(CNSocket* sock, CNPacketData* data) {
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
sP_CL2FE_REQ_PC_SLOPE* slopeData = (sP_CL2FE_REQ_PC_SLOPE*)data->buf;
|
||||
PlayerManager::updatePlayerPosition(sock, slopeData->iX, slopeData->iY, slopeData->iZ, plr->instanceID, slopeData->iAngle);
|
||||
|
||||
uint64_t tm = getTime();
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_SLOPE, slopeResponse);
|
||||
|
||||
slopeResponse.iPC_ID = plr->iID;
|
||||
slopeResponse.iCliTime = slopeData->iCliTime;
|
||||
slopeResponse.iSvrTime = tm;
|
||||
slopeResponse.iX = slopeData->iX;
|
||||
slopeResponse.iY = slopeData->iY;
|
||||
slopeResponse.iZ = slopeData->iZ;
|
||||
slopeResponse.iAngle = slopeData->iAngle;
|
||||
slopeResponse.fVX = slopeData->fVX;
|
||||
slopeResponse.fVY = slopeData->fVY;
|
||||
slopeResponse.fVZ = slopeData->fVZ;
|
||||
slopeResponse.iSpeed = slopeData->iSpeed;
|
||||
slopeResponse.cKeyValue = slopeData->cKeyValue;
|
||||
slopeResponse.iSlopeID = slopeData->iSlopeID;
|
||||
|
||||
PlayerManager::sendToViewable(sock, slopeResponse, P_FE2CL_PC_SLOPE);
|
||||
}
|
||||
|
||||
void PlayerMovement::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_MOVE, movePlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_STOP, stopPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_JUMP, jumpPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_JUMPPAD, jumppadPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_LAUNCHER, launchPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ZIPLINE, ziplinePlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_MOVEPLATFORM, movePlatformPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_MOVETRANSPORTATION, moveSliderPlayer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SLOPE, moveSlopePlayer);
|
||||
}
|
||||
5
src/PlayerMovement.hpp
Normal file
5
src/PlayerMovement.hpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace PlayerMovement {
|
||||
void init();
|
||||
};
|
||||
178
src/Racing.cpp
Normal file
178
src/Racing.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "Racing.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Missions.hpp"
|
||||
#include "Items.hpp"
|
||||
#include "db/Database.hpp"
|
||||
#include "NPCManager.hpp"
|
||||
|
||||
using namespace Racing;
|
||||
|
||||
std::map<int32_t, EPInfo> Racing::EPData;
|
||||
std::map<CNSocket*, EPRace> Racing::EPRaces;
|
||||
std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> Racing::EPRewards;
|
||||
|
||||
static void racingStart(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_EP_RACE_START*)data->buf;
|
||||
|
||||
if (NPCManager::NPCs.find(req->iStartEcomID) == NPCManager::NPCs.end())
|
||||
return; // starting line agent not found
|
||||
|
||||
int mapNum = MAPNUM(NPCManager::NPCs[req->iStartEcomID]->instanceID);
|
||||
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
|
||||
return; // IZ not found
|
||||
|
||||
// make ongoing race entry
|
||||
EPRace race = { {}, req->iEPRaceMode, req->iEPTicketItemSlotNum, getTime() / 1000 };
|
||||
EPRaces[sock] = race;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_RACE_START_SUCC, resp);
|
||||
resp.iStartTick = 0; // ignored
|
||||
resp.iLimitTime = EPData[mapNum].maxTime;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_START_SUCC);
|
||||
}
|
||||
|
||||
static void racingGetPod(CNSocket* sock, CNPacketData* data) {
|
||||
if (EPRaces.find(sock) == EPRaces.end())
|
||||
return; // race not found
|
||||
|
||||
auto req = (sP_CL2FE_REQ_EP_GET_RING*)data->buf;
|
||||
|
||||
if (EPRaces[sock].collectedRings.count(req->iRingLID))
|
||||
return; // can't collect the same ring twice
|
||||
|
||||
// without an anticheat system, we really don't have a choice but to honor the request
|
||||
// TODO: proximity check so players can't cheat the race by replaying packets
|
||||
EPRaces[sock].collectedRings.insert(req->iRingLID);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_GET_RING_SUCC, resp);
|
||||
|
||||
resp.iRingLID = req->iRingLID;
|
||||
resp.iRingCount_Get = EPRaces[sock].collectedRings.size();
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_EP_GET_RING_SUCC);
|
||||
}
|
||||
|
||||
static void racingCancel(CNSocket* sock, CNPacketData* data) {
|
||||
if (EPRaces.find(sock) == EPRaces.end())
|
||||
return; // race not found
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
EPRaces.erase(sock);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
|
||||
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
|
||||
|
||||
/*
|
||||
* This request packet is used for both cancelling the race via the
|
||||
* NPC at the start, *and* failing the race by running out of time.
|
||||
* If the latter is to happen, the client disables movement until it
|
||||
* receives a packet from the server that re-enables it.
|
||||
*
|
||||
* So, in order to prevent a potential softlock we respawn the player.
|
||||
*/
|
||||
|
||||
WarpLocation* respawnLoc = PlayerManager::getRespawnPoint(plr);
|
||||
|
||||
if (respawnLoc != nullptr) {
|
||||
PlayerManager::sendPlayerTo(sock, respawnLoc->x, respawnLoc->y, respawnLoc->z, respawnLoc->instanceID);
|
||||
} else {
|
||||
// fallback, just respawn the player in-place if no suitable point is found
|
||||
PlayerManager::sendPlayerTo(sock, plr->x, plr->y, plr->z, plr->instanceID);
|
||||
}
|
||||
}
|
||||
|
||||
static void racingEnd(CNSocket* sock, CNPacketData* data) {
|
||||
if (EPRaces.find(sock) == EPRaces.end())
|
||||
return; // race not found
|
||||
|
||||
auto req = (sP_CL2FE_REQ_EP_RACE_END*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (NPCManager::NPCs.find(req->iEndEcomID) == NPCManager::NPCs.end())
|
||||
return; // finish line agent not found
|
||||
|
||||
int mapNum = MAPNUM(NPCManager::NPCs[req->iEndEcomID]->instanceID);
|
||||
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
|
||||
return; // IZ not found
|
||||
|
||||
uint64_t now = getTime() / 1000;
|
||||
|
||||
int timeDiff = now - EPRaces[sock].startTime;
|
||||
int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff;
|
||||
if (score < 0) score = 0; // lol
|
||||
int fm = score * plr->level * (1.0f / 36) * 0.3f;
|
||||
|
||||
// we submit the ranking first...
|
||||
Database::RaceRanking postRanking = {};
|
||||
postRanking.EPID = EPData[mapNum].EPID;
|
||||
postRanking.PlayerID = plr->iID;
|
||||
postRanking.RingCount = EPRaces[sock].collectedRings.size();
|
||||
postRanking.Score = score;
|
||||
postRanking.Time = timeDiff;
|
||||
postRanking.Timestamp = getTimestamp();
|
||||
Database::postRaceRanking(postRanking);
|
||||
|
||||
// ...then we get the top ranking, which may or may not be what we just submitted
|
||||
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(EPData[mapNum].EPID, plr->iID);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
|
||||
|
||||
// get rank scores and rewards
|
||||
std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first;
|
||||
std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second;
|
||||
|
||||
// top ranking
|
||||
int topRank = 0;
|
||||
while (rankScores->at(topRank) > topRankingPlayer.Score)
|
||||
topRank++;
|
||||
|
||||
resp.iEPTopRank = topRank + 1;
|
||||
resp.iEPTopRingCount = topRankingPlayer.RingCount;
|
||||
resp.iEPTopScore = topRankingPlayer.Score;
|
||||
resp.iEPTopTime = topRankingPlayer.Time;
|
||||
|
||||
// this ranking
|
||||
int rank = 0;
|
||||
while (rankScores->at(rank) > postRanking.Score)
|
||||
rank++;
|
||||
|
||||
resp.iEPRank = rank + 1;
|
||||
resp.iEPRingCnt = postRanking.RingCount;
|
||||
resp.iEPScore = postRanking.Score;
|
||||
resp.iEPRaceTime = postRanking.Time;
|
||||
resp.iEPRaceMode = EPRaces[sock].mode;
|
||||
resp.iEPRewardFM = fm;
|
||||
|
||||
Missions::updateFusionMatter(sock, resp.iEPRewardFM);
|
||||
|
||||
resp.iFusionMatter = plr->fusionmatter;
|
||||
resp.iFatigue = 50;
|
||||
resp.iFatigue_Level = 1;
|
||||
|
||||
sItemReward reward;
|
||||
reward.iSlotNum = Items::findFreeSlot(plr);
|
||||
reward.eIL = 1;
|
||||
sItemBase item;
|
||||
item.iID = rankRewards->at(rank); // rank scores and rewards line up
|
||||
item.iType = 9;
|
||||
item.iOpt = 1;
|
||||
item.iTimeLimit = 0;
|
||||
reward.sItem = item;
|
||||
|
||||
if (reward.iSlotNum > -1 && reward.sItem.iID != 0) {
|
||||
resp.RewardItem = reward;
|
||||
plr->Inven[reward.iSlotNum] = item;
|
||||
}
|
||||
|
||||
EPRaces.erase(sock);
|
||||
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_END_SUCC);
|
||||
}
|
||||
|
||||
void Racing::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_START, racingStart);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_GET_RING, racingGetPod);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_CANCEL, racingCancel);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_END, racingEnd);
|
||||
}
|
||||
23
src/Racing.hpp
Normal file
23
src/Racing.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
struct EPInfo {
|
||||
int zoneX, zoneY, EPID, maxScore, maxTime;
|
||||
};
|
||||
|
||||
struct EPRace {
|
||||
std::set<int> collectedRings;
|
||||
int mode, ticketSlot;
|
||||
time_t startTime;
|
||||
};
|
||||
|
||||
namespace Racing {
|
||||
extern std::map<int32_t, EPInfo> EPData;
|
||||
extern std::map<CNSocket*, EPRace> EPRaces;
|
||||
extern std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> EPRewards;
|
||||
|
||||
void init();
|
||||
}
|
||||
91
src/Rand.cpp
Normal file
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));
|
||||
}
|
||||
22
src/Rand.hpp
Normal file
22
src/Rand.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <random>
|
||||
#include <memory>
|
||||
|
||||
namespace Rand {
|
||||
extern std::unique_ptr<std::mt19937> generator;
|
||||
|
||||
void init(uint64_t seed);
|
||||
|
||||
int32_t rand(int32_t startInclusive, int32_t endExclusive);
|
||||
int32_t rand(int32_t endExclusive);
|
||||
int32_t rand();
|
||||
|
||||
int32_t randWeighted(const std::vector<int32_t>& weights);
|
||||
|
||||
uint64_t cryptoRand();
|
||||
|
||||
float randFloat(float startInclusive, float endExclusive);
|
||||
float randFloat(float endExclusive);
|
||||
float randFloat();
|
||||
};
|
||||
1369
src/TableData.cpp
Normal file
1369
src/TableData.cpp
Normal file
File diff suppressed because it is too large
Load Diff
27
src/TableData.hpp
Normal file
27
src/TableData.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "NPCManager.hpp"
|
||||
|
||||
// these are added to the NPC's static key to avoid collisions
|
||||
const int NPC_ID_OFFSET = 1;
|
||||
const int MOB_ID_OFFSET = 10000;
|
||||
const int MOB_GROUP_ID_OFFSET = 20000;
|
||||
|
||||
// typedef for JSON object because I don't want to type nlohmann::json every time
|
||||
typedef nlohmann::json json;
|
||||
|
||||
namespace TableData {
|
||||
extern std::map<int32_t, std::vector<Vec3>> RunningSkywayRoutes;
|
||||
extern std::map<int32_t, int> RunningNPCRotations;
|
||||
extern std::map<int32_t, int> RunningNPCMapNumbers;
|
||||
extern std::unordered_map<int32_t, std::pair<BaseNPC*, std::vector<BaseNPC*>>> RunningNPCPaths; // player ID -> following NPC
|
||||
extern std::vector<NPCPath> FinishedNPCPaths; // NPC ID -> path
|
||||
extern std::map<int32_t, BaseNPC*> RunningMobs;
|
||||
extern std::map<int32_t, BaseNPC*> RunningGroups;
|
||||
extern std::map<int32_t, BaseNPC*> RunningEggs;
|
||||
|
||||
void init();
|
||||
void flush();
|
||||
}
|
||||
435
src/Trading.cpp
Normal file
435
src/Trading.cpp
Normal file
@@ -0,0 +1,435 @@
|
||||
#include "Trading.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "db/Database.hpp"
|
||||
|
||||
using namespace Trading;
|
||||
|
||||
static bool doTrade(Player* plr, Player* plr2) {
|
||||
// init dummy inventories
|
||||
sItemBase plrInven[AINVEN_COUNT];
|
||||
sItemBase plr2Inven[AINVEN_COUNT];
|
||||
memcpy(plrInven, plr->Inven, AINVEN_COUNT * sizeof(sItemBase));
|
||||
memcpy(plr2Inven, plr2->Inven, AINVEN_COUNT * sizeof(sItemBase));
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// remove items offered by us
|
||||
if (plr->Trade[i].iID != 0) {
|
||||
if (plrInven[plr->Trade[i].iInvenNum].iID == 0
|
||||
|| plr->Trade[i].iID != plrInven[plr->Trade[i].iInvenNum].iID
|
||||
|| plr->Trade[i].iType != plrInven[plr->Trade[i].iInvenNum].iType) // pulling a fast one on us
|
||||
return false;
|
||||
|
||||
if (plr->Trade[i].iOpt < 1) {
|
||||
std::cout << "[WARN] Player tried trading an iOpt < 1 amount" << std::endl;
|
||||
plr->Trade[i].iOpt = 1;
|
||||
}
|
||||
|
||||
// for stacked items
|
||||
plrInven[plr->Trade[i].iInvenNum].iOpt -= plr->Trade[i].iOpt;
|
||||
|
||||
if (plrInven[plr->Trade[i].iInvenNum].iOpt == 0) {
|
||||
plrInven[plr->Trade[i].iInvenNum].iID = 0;
|
||||
plrInven[plr->Trade[i].iInvenNum].iType = 0;
|
||||
plrInven[plr->Trade[i].iInvenNum].iOpt = 0;
|
||||
} else if (plrInven[plr->Trade[i].iInvenNum].iOpt < 0) { // another dupe attempt
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (plr2->Trade[i].iID != 0) {
|
||||
if (plr2Inven[plr2->Trade[i].iInvenNum].iID == 0
|
||||
|| plr2->Trade[i].iID != plr2Inven[plr2->Trade[i].iInvenNum].iID
|
||||
|| plr2->Trade[i].iType != plr2Inven[plr2->Trade[i].iInvenNum].iType) // pulling a fast one on us
|
||||
return false;
|
||||
|
||||
if (plr2->Trade[i].iOpt < 1) {
|
||||
std::cout << "[WARN] Player tried trading an iOpt < 1 amount" << std::endl;
|
||||
plr2->Trade[i].iOpt = 1;
|
||||
}
|
||||
|
||||
// for stacked items
|
||||
plr2Inven[plr2->Trade[i].iInvenNum].iOpt -= plr2->Trade[i].iOpt;
|
||||
|
||||
if (plr2Inven[plr2->Trade[i].iInvenNum].iOpt == 0) {
|
||||
plr2Inven[plr2->Trade[i].iInvenNum].iID = 0;
|
||||
plr2Inven[plr2->Trade[i].iInvenNum].iType = 0;
|
||||
plr2Inven[plr2->Trade[i].iInvenNum].iOpt = 0;
|
||||
} else if (plr2Inven[plr2->Trade[i].iInvenNum].iOpt < 0) { // another dupe attempt
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// add items offered to us
|
||||
if (plr2->Trade[i].iID != 0) {
|
||||
for (int n = 0; n < AINVEN_COUNT; n++) {
|
||||
if (plrInven[n].iID == 0) {
|
||||
plrInven[n].iID = plr2->Trade[i].iID;
|
||||
plrInven[n].iType = plr2->Trade[i].iType;
|
||||
plrInven[n].iOpt = plr2->Trade[i].iOpt;
|
||||
plr2->Trade[i].iInvenNum = n;
|
||||
break;
|
||||
}
|
||||
|
||||
if (n >= AINVEN_COUNT - 1)
|
||||
return false; // not enough space
|
||||
}
|
||||
}
|
||||
|
||||
if (plr->Trade[i].iID != 0) {
|
||||
for (int n = 0; n < AINVEN_COUNT; n++) {
|
||||
if (plr2Inven[n].iID == 0) {
|
||||
plr2Inven[n].iID = plr->Trade[i].iID;
|
||||
plr2Inven[n].iType = plr->Trade[i].iType;
|
||||
plr2Inven[n].iOpt = plr->Trade[i].iOpt;
|
||||
plr->Trade[i].iInvenNum = n;
|
||||
break;
|
||||
}
|
||||
|
||||
if (n >= AINVEN_COUNT - 1)
|
||||
return false; // not enough space
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if everything went well, back into player inventory it goes
|
||||
memcpy(plr->Inven, plrInven, AINVEN_COUNT * sizeof(sItemBase));
|
||||
memcpy(plr2->Inven, plr2Inven, AINVEN_COUNT * sizeof(sItemBase));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void tradeOffer(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_OFFER* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
if (plr->isTrading) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, resp);
|
||||
resp.iID_Request = pacdat->iID_To;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
|
||||
return; // prevent trading with a player already trading
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER));
|
||||
}
|
||||
|
||||
static void tradeOfferAccept(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
if (plr2->isTrading) {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, resp);
|
||||
resp.iID_Request = pacdat->iID_From;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
|
||||
return; // prevent trading with a player already trading
|
||||
}
|
||||
|
||||
// clearing up trade slots
|
||||
plr->moneyInTrade = 0;
|
||||
plr2->moneyInTrade = 0;
|
||||
memset(&plr->Trade, 0, sizeof(plr->Trade));
|
||||
memset(&plr2->Trade, 0, sizeof(plr2->Trade));
|
||||
|
||||
// marking players as traders
|
||||
plr->isTrading = true;
|
||||
plr2->isTrading = true;
|
||||
|
||||
// marking players as unconfirmed
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
// inform the other player that offer is accepted
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_SUCC));
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_SUCC));
|
||||
}
|
||||
|
||||
static void tradeOfferRefusal(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL*)data->buf;
|
||||
|
||||
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
|
||||
}
|
||||
|
||||
static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf;
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
if (!(plr->isTrading && plr2->isTrading)) { // both players must be trading
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp);
|
||||
resp.iID_Request = plr2->iID;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
|
||||
resp.iID_Request = plr->iID;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
|
||||
|
||||
// both players are no longer trading
|
||||
plr->isTrading = false;
|
||||
plr2->isTrading = false;
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// send the confirm packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM));
|
||||
|
||||
if (!(plr2->isTradeConfirm)) {
|
||||
plr->isTradeConfirm = true;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM));
|
||||
return;
|
||||
}
|
||||
|
||||
// both players are no longer trading
|
||||
plr->isTrading = false;
|
||||
plr2->isTrading = false;
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
if (doTrade(plr, plr2)) { // returns false if not enough slots
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, resp2);
|
||||
resp2.iID_Request = pacdat->iID_Request;
|
||||
resp2.iID_From = pacdat->iID_From;
|
||||
resp2.iID_To = pacdat->iID_To;
|
||||
plr->money = plr->money + plr2->moneyInTrade - plr->moneyInTrade;
|
||||
resp2.iCandy = plr->money;
|
||||
memcpy(resp2.Item, plr2->Trade, sizeof(plr2->Trade));
|
||||
memcpy(resp2.ItemStay, plr->Trade, sizeof(plr->Trade));
|
||||
|
||||
sock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
|
||||
|
||||
plr2->money = plr2->money + plr->moneyInTrade - plr2->moneyInTrade;
|
||||
resp2.iCandy = plr2->money;
|
||||
memcpy(resp2.Item, plr->Trade, sizeof(plr->Trade));
|
||||
memcpy(resp2.ItemStay, plr2->Trade, sizeof(plr2->Trade));
|
||||
|
||||
otherSock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
|
||||
} else {
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp);
|
||||
resp.iID_Request = plr->iID;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
|
||||
resp.iID_Request = plr2->iID;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
|
||||
|
||||
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
|
||||
std::string text = "Trade Failed";
|
||||
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
|
||||
msg.iDuringTime = 3;
|
||||
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
otherSock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
|
||||
return;
|
||||
}
|
||||
|
||||
Database::commitTrade(plr, plr2);
|
||||
}
|
||||
|
||||
static void tradeConfirmCancel(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL*)data->buf;
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
|
||||
// both players are not trading nor are in a confirmed state
|
||||
plr->isTrading = false;
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTrading = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL));
|
||||
}
|
||||
|
||||
static void tradeRegisterItem(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_ITEM_REGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_ITEM_REGISTER*)data->buf;
|
||||
|
||||
if (pacdat->Item.iSlotNum < 0 || pacdat->Item.iSlotNum > 4)
|
||||
return; // sanity check, there are only 5 trade slots
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
plr->Trade[pacdat->Item.iSlotNum] = pacdat->Item;
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
// since you can spread items like gumballs over multiple slots, we need to count them all
|
||||
// to make sure the inventory shows the right value during trade.
|
||||
int count = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (plr->Trade[i].iInvenNum == pacdat->Item.iInvenNum)
|
||||
count += plr->Trade[i].iOpt;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
resp.TradeItem = pacdat->Item;
|
||||
resp.InvenItem = pacdat->Item;
|
||||
resp.InvenItem.iOpt = plr->Inven[pacdat->Item.iInvenNum].iOpt - count; // subtract this count
|
||||
|
||||
if (resp.InvenItem.iOpt < 0) // negative count items, doTrade() will block this later on
|
||||
std::cout << "[WARN] tradeRegisterItem: an item went negative count client side." << std::endl;
|
||||
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC));
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC));
|
||||
}
|
||||
|
||||
static void tradeUnregisterItem(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER*)data->buf;
|
||||
|
||||
if (pacdat->Item.iSlotNum < 0 || pacdat->Item.iSlotNum > 4)
|
||||
return; // sanity check, there are only 5 trade slots
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
resp.TradeItem = pacdat->Item;
|
||||
resp.InvenItem = plr->Trade[pacdat->Item.iSlotNum];
|
||||
|
||||
memset(&plr->Trade[pacdat->Item.iSlotNum], 0, sizeof(plr->Trade[pacdat->Item.iSlotNum])); // clean up item slot
|
||||
|
||||
// since you can spread items like gumballs over multiple slots, we need to count them all
|
||||
// to make sure the inventory shows the right value during trade.
|
||||
int count = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (plr->Trade[i].iInvenNum == resp.InvenItem.iInvenNum)
|
||||
count += plr->Trade[i].iOpt;
|
||||
}
|
||||
|
||||
resp.InvenItem.iOpt = plr->Inven[resp.InvenItem.iInvenNum].iOpt - count; // subtract this count
|
||||
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC));
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC));
|
||||
}
|
||||
|
||||
static void tradeRegisterCash(CNSocket* sock, CNPacketData* data) {
|
||||
sP_CL2FE_REQ_PC_TRADE_CASH_REGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_CASH_REGISTER*)data->buf;
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
if (pacdat->iCandy < 0 || pacdat->iCandy > plr->money)
|
||||
return; // famous glitch, begone
|
||||
|
||||
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
|
||||
if (pacdat->iID_Request == pacdat->iID_From)
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
|
||||
else
|
||||
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
|
||||
|
||||
if (otherSock == nullptr)
|
||||
return;
|
||||
|
||||
Player* plr2 = PlayerManager::getPlayer(otherSock);
|
||||
plr->isTradeConfirm = false;
|
||||
plr2->isTradeConfirm = false;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, resp);
|
||||
resp.iID_Request = pacdat->iID_Request;
|
||||
resp.iID_From = pacdat->iID_From;
|
||||
resp.iID_To = pacdat->iID_To;
|
||||
resp.iCandy = pacdat->iCandy;
|
||||
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
|
||||
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
|
||||
|
||||
plr->moneyInTrade = pacdat->iCandy;
|
||||
plr->isTradeConfirm = false;
|
||||
}
|
||||
|
||||
void Trading::init() {
|
||||
// Trade handlers
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, tradeOffer);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT, tradeOfferAccept);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, tradeOfferRefusal);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM, tradeConfirm);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER, tradeUnregisterItem);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CASH_REGISTER, tradeRegisterCash);
|
||||
}
|
||||
7
src/Trading.hpp
Normal file
7
src/Trading.hpp
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Items.hpp"
|
||||
|
||||
namespace Trading {
|
||||
void init();
|
||||
}
|
||||
425
src/Transport.cpp
Normal file
425
src/Transport.cpp
Normal file
@@ -0,0 +1,425 @@
|
||||
#include "servers/CNShardServer.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Nanos.hpp"
|
||||
#include "Transport.hpp"
|
||||
#include "TableData.hpp"
|
||||
#include "Combat.hpp"
|
||||
#include "MobAI.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <cmath>
|
||||
|
||||
using namespace Transport;
|
||||
|
||||
std::map<int32_t, TransportRoute> Transport::Routes;
|
||||
std::map<int32_t, TransportLocation> Transport::Locations;
|
||||
std::vector<NPCPath> Transport::NPCPaths;
|
||||
std::map<int32_t, std::queue<Vec3>> Transport::SkywayPaths;
|
||||
std::unordered_map<CNSocket*, std::queue<Vec3>> Transport::SkywayQueues;
|
||||
std::unordered_map<int32_t, std::queue<Vec3>> Transport::NPCQueues;
|
||||
|
||||
static void transportRegisterLocationHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto transport = (sP_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
bool newReg = false; // this is a new registration
|
||||
//std::cout << "request to register transport, eTT " << transport->eTT << ", locID " << transport->iLocationID << ", npc " << transport->iNPC_ID << std::endl;
|
||||
if (transport->eTT == 1) { // S.C.A.M.P.E.R.
|
||||
if (transport->iLocationID < 1 || transport->iLocationID > 31) { // sanity check
|
||||
std::cout << "[WARN] S.C.A.M.P.E.R. location ID " << transport->iLocationID << " is out of bounds" << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);
|
||||
|
||||
failResp.eTT = transport->eTT;
|
||||
failResp.iErrorCode = 0; // TODO: review what error code to use here
|
||||
failResp.iLocationID = transport->iLocationID;
|
||||
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// update registration bitfield using bitmask
|
||||
uint32_t newScamperFlag = plr->iWarpLocationFlag | (1UL << (transport->iLocationID - 1));
|
||||
if (newScamperFlag != plr->iWarpLocationFlag) {
|
||||
plr->iWarpLocationFlag = newScamperFlag;
|
||||
newReg = true;
|
||||
}
|
||||
} else if (transport->eTT == 2) { // Monkey Skyway System
|
||||
if (transport->iLocationID < 1 || transport->iLocationID > 127) { // sanity check
|
||||
std::cout << "[WARN] Skyway location ID " << transport->iLocationID << " is out of bounds" << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);
|
||||
|
||||
failResp.eTT = transport->eTT;
|
||||
failResp.iErrorCode = 0; // TODO: review what error code to use here
|
||||
failResp.iLocationID = transport->iLocationID;
|
||||
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* assuming the two bitfields are just stuck together to make a longer one, do a similar operation
|
||||
*/
|
||||
int index = transport->iLocationID > 64 ? 1 : 0;
|
||||
uint64_t newMonkeyFlag = plr->aSkywayLocationFlag[index] | (1ULL << (index ? transport->iLocationID - 65 : transport->iLocationID - 1));
|
||||
if (newMonkeyFlag != plr->aSkywayLocationFlag[index]) {
|
||||
plr->aSkywayLocationFlag[index] = newMonkeyFlag;
|
||||
newReg = true;
|
||||
}
|
||||
} else {
|
||||
std::cout << "[WARN] Unknown mode of transport; eTT = " << transport->eTT << std::endl;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);
|
||||
|
||||
failResp.eTT = transport->eTT;
|
||||
failResp.iErrorCode = 0; // TODO: review what error code to use here
|
||||
failResp.iLocationID = transport->iLocationID;
|
||||
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newReg)
|
||||
return; // don't send new registration message
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC, resp);
|
||||
// response parameters
|
||||
resp.eTT = transport->eTT;
|
||||
resp.iLocationID = transport->iLocationID;
|
||||
resp.iWarpLocationFlag = plr->iWarpLocationFlag;
|
||||
resp.aWyvernLocationFlag[0] = plr->aSkywayLocationFlag[0];
|
||||
resp.aWyvernLocationFlag[1] = plr->aSkywayLocationFlag[1];
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC);
|
||||
}
|
||||
|
||||
static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
/*
|
||||
* req:
|
||||
* eIL -- inventory type
|
||||
* iNPC_ID -- the ID of the NPC who is warping you
|
||||
* iTransporationID -- iVehicleID
|
||||
* iSlotNum -- inventory slot number
|
||||
*/
|
||||
|
||||
if (Routes.find(req->iTransporationID) == Routes.end() || Routes[req->iTransporationID].cost > plr->money) { // sanity check
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL, failResp);
|
||||
|
||||
failResp.iErrorCode = 0; // TODO: error code
|
||||
failResp.iTransportationID = req->iTransporationID;
|
||||
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
TransportRoute route = Routes[req->iTransporationID];
|
||||
plr->money -= route.cost;
|
||||
|
||||
TransportLocation* target = nullptr;
|
||||
switch (route.type) {
|
||||
case 1: // S.C.A.M.P.E.R.
|
||||
target = &Locations[route.end];
|
||||
break;
|
||||
case 2: // Monkey Skyway
|
||||
// set last safe coords
|
||||
plr->lastX = plr->x;
|
||||
plr->lastY = plr->y;
|
||||
plr->lastZ = plr->z;
|
||||
if (SkywayPaths.find(route.mssRouteNum) != SkywayPaths.end()) { // check if route exists
|
||||
Nanos::summonNano(sock, -1); // make sure that no nano is active during the ride
|
||||
SkywayQueues[sock] = SkywayPaths[route.mssRouteNum]; // set socket point queue to route
|
||||
plr->onMonkey = true;
|
||||
break;
|
||||
} else if (TableData::RunningSkywayRoutes.find(route.mssRouteNum) != TableData::RunningSkywayRoutes.end()) {
|
||||
std::vector<Vec3>* _route = &TableData::RunningSkywayRoutes[route.mssRouteNum];
|
||||
Nanos::summonNano(sock, -1);
|
||||
testMssRoute(sock, _route);
|
||||
plr->onMonkey = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// refund and send alert packet
|
||||
plr->money += route.cost;
|
||||
INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert);
|
||||
alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog
|
||||
alert.iDuringTime = 3;
|
||||
U8toU16("Skyway route " + std::to_string(route.mssRouteNum) + " isn't pathed yet. You will not be charged any taros.", (char16_t*)alert.szAnnounceMsg, sizeof(alert.szAnnounceMsg));
|
||||
sock->sendPacket(alert, P_FE2CL_ANNOUNCE_MSG);
|
||||
|
||||
std::cout << "[WARN] MSS route " << route.mssRouteNum << " not pathed" << std::endl;
|
||||
break;
|
||||
default:
|
||||
std::cout << "[WARN] Unknown tranportation type " << route.type << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC, resp);
|
||||
// response parameters
|
||||
resp.eTT = route.type;
|
||||
resp.iCandy = plr->money;
|
||||
resp.iX = (target == nullptr) ? plr->x : target->x;
|
||||
resp.iY = (target == nullptr) ? plr->y : target->y;
|
||||
resp.iZ = (target == nullptr) ? plr->z : target->z;
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC);
|
||||
|
||||
if (target == nullptr)
|
||||
return;
|
||||
|
||||
// we warped; update position and chunks
|
||||
PlayerManager::updatePlayerPositionForWarp(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD);
|
||||
}
|
||||
|
||||
void Transport::testMssRoute(CNSocket *sock, std::vector<Vec3>* route) {
|
||||
int speed = 1500; // TODO: make this adjustable
|
||||
std::queue<Vec3> path;
|
||||
Vec3 last = route->front(); // start pos
|
||||
|
||||
for (int i = 1; i < route->size(); i++) {
|
||||
Vec3 coords = route->at(i);
|
||||
Transport::lerp(&path, last, coords, speed);
|
||||
path.push(coords); // add keyframe to the queue
|
||||
last = coords; // update start pos
|
||||
}
|
||||
|
||||
SkywayQueues[sock] = path;
|
||||
}
|
||||
|
||||
/*
|
||||
* Go through every socket that has broomstick points queued up, and advance to the next point.
|
||||
* If the player has disconnected or finished the route, clean up and remove them from the queue.
|
||||
*/
|
||||
static void stepSkywaySystem() {
|
||||
|
||||
// using an unordered map so we can remove finished players in one iteration
|
||||
std::unordered_map<CNSocket*, std::queue<Vec3>>::iterator it = SkywayQueues.begin();
|
||||
while (it != SkywayQueues.end()) {
|
||||
|
||||
std::queue<Vec3>* queue = &it->second;
|
||||
|
||||
if (PlayerManager::players.find(it->first) == PlayerManager::players.end()) {
|
||||
// pluck out dead socket + update iterator
|
||||
it = SkywayQueues.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(it->first);
|
||||
|
||||
if (queue->empty()) {
|
||||
// send dismount packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_RIDING_SUCC, rideSucc);
|
||||
INITSTRUCT(sP_FE2CL_PC_RIDING, rideBroadcast);
|
||||
rideSucc.iPC_ID = plr->iID;
|
||||
rideSucc.eRT = 0;
|
||||
rideBroadcast.iPC_ID = plr->iID;
|
||||
rideBroadcast.eRT = 0;
|
||||
it->first->sendPacket(rideSucc, P_FE2CL_REP_PC_RIDING_SUCC);
|
||||
// send packet to players in view
|
||||
PlayerManager::sendToViewable(it->first, rideBroadcast, P_FE2CL_PC_RIDING);
|
||||
it = SkywayQueues.erase(it); // remove player from tracking map + update iterator
|
||||
plr->onMonkey = false;
|
||||
} else {
|
||||
Vec3 point = queue->front(); // get point
|
||||
queue->pop(); // remove point from front of queue
|
||||
|
||||
INITSTRUCT(sP_FE2CL_PC_BROOMSTICK_MOVE, bmstk);
|
||||
bmstk.iPC_ID = plr->iID;
|
||||
bmstk.iToX = point.x;
|
||||
bmstk.iToY = point.y;
|
||||
bmstk.iToZ = point.z;
|
||||
it->first->sendPacket(bmstk, P_FE2CL_PC_BROOMSTICK_MOVE);
|
||||
// set player location to point to update viewables
|
||||
PlayerManager::updatePlayerPosition(it->first, point.x, point.y, point.z, plr->instanceID, plr->angle);
|
||||
// send packet to players in view
|
||||
PlayerManager::sendToViewable(it->first, bmstk, P_FE2CL_PC_BROOMSTICK_MOVE);
|
||||
|
||||
it++; // go to next entry in map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void stepNPCPathing() {
|
||||
|
||||
// all NPC pathing queues
|
||||
std::unordered_map<int32_t, std::queue<Vec3>>::iterator it = NPCQueues.begin();
|
||||
while (it != NPCQueues.end()) {
|
||||
|
||||
std::queue<Vec3>* queue = &it->second;
|
||||
|
||||
BaseNPC* npc = nullptr;
|
||||
if (NPCManager::NPCs.find(it->first) != NPCManager::NPCs.end())
|
||||
npc = NPCManager::NPCs[it->first];
|
||||
|
||||
if (npc == nullptr || queue->empty()) {
|
||||
// pluck out dead path + update iterator
|
||||
it = NPCQueues.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip if not simulating mobs
|
||||
if (npc->type == EntityType::MOB && !MobAI::simulateMobs) {
|
||||
it++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// do not roam if not roaming
|
||||
if (npc->type == EntityType::MOB && ((Mob*)npc)->state != MobState::ROAMING) {
|
||||
it++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Vec3 point = queue->front(); // get point
|
||||
queue->pop(); // remove point from front of queue
|
||||
|
||||
// calculate displacement
|
||||
int dXY = hypot(point.x - npc->x, point.y - npc->y); // XY plane distance
|
||||
int distanceBetween = hypot(dXY, point.z - npc->z); // total distance
|
||||
|
||||
// update NPC location to update viewables
|
||||
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, point.x, point.y, point.z, npc->instanceID, npc->appearanceData.iAngle);
|
||||
|
||||
// TODO: move walking logic into Entity stack
|
||||
switch (npc->type) {
|
||||
case EntityType::BUS:
|
||||
INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove);
|
||||
|
||||
busMove.eTT = 3;
|
||||
busMove.iT_ID = npc->appearanceData.iNPC_ID;
|
||||
busMove.iMoveStyle = 0; // ???
|
||||
busMove.iToX = point.x;
|
||||
busMove.iToY = point.y;
|
||||
busMove.iToZ = point.z;
|
||||
busMove.iSpeed = distanceBetween; // set to distance to match how monkeys work
|
||||
|
||||
NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE));
|
||||
break;
|
||||
case EntityType::MOB:
|
||||
MobAI::incNextMovement((Mob*)npc);
|
||||
/* fallthrough */
|
||||
default:
|
||||
INITSTRUCT(sP_FE2CL_NPC_MOVE, move);
|
||||
move.iNPC_ID = npc->appearanceData.iNPC_ID;
|
||||
move.iMoveStyle = 0; // ???
|
||||
move.iToX = point.x;
|
||||
move.iToY = point.y;
|
||||
move.iToZ = point.z;
|
||||
move.iSpeed = distanceBetween;
|
||||
|
||||
NPCManager::sendToViewable(npc, &move, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* If this path should be repeated, move processed point to the back to maintain cycle.
|
||||
*/
|
||||
if (npc->loopingPath)
|
||||
queue->push(point);
|
||||
|
||||
it++; // go to next entry in map
|
||||
}
|
||||
}
|
||||
|
||||
static void tickTransportationSystem(CNServer* serv, time_t currTime) {
|
||||
stepNPCPathing();
|
||||
stepSkywaySystem();
|
||||
}
|
||||
|
||||
/*
|
||||
* Linearly interpolate between two points and insert the results into a queue.
|
||||
*/
|
||||
void Transport::lerp(std::queue<Vec3>* queue, Vec3 start, Vec3 end, int gapSize, float curve) {
|
||||
int dXY = hypot(end.x - start.x, end.y - start.y); // XY plane distance
|
||||
int distanceBetween = hypot(dXY, end.z - start.z); // total distance
|
||||
int lerps = distanceBetween / gapSize; // number of intermediate points to add
|
||||
for (int i = 1; i <= lerps; i++) {
|
||||
Vec3 lerp;
|
||||
// lerp math
|
||||
//float frac = i / (lerps + 1);
|
||||
float frac = powf(i, curve) / powf(lerps + 1, curve);
|
||||
lerp.x = (start.x * (1.0f - frac)) + (end.x * frac);
|
||||
lerp.y = (start.y * (1.0f - frac)) + (end.y * frac);
|
||||
lerp.z = (start.z * (1.0f - frac)) + (end.z * frac);
|
||||
queue->push(lerp); // add lerp'd point
|
||||
}
|
||||
}
|
||||
void Transport::lerp(std::queue<Vec3>* queue, Vec3 start, Vec3 end, int gapSize) {
|
||||
lerp(queue, start, end, gapSize, 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Find and return the first path that targets either the type or the ID.
|
||||
* If no matches are found, return nullptr
|
||||
*/
|
||||
NPCPath* Transport::findApplicablePath(int32_t id, int32_t type, int taskID) {
|
||||
NPCPath* match = nullptr;
|
||||
for (auto _path = Transport::NPCPaths.begin(); _path != Transport::NPCPaths.end(); _path++) {
|
||||
|
||||
// task ID for the path must match so escorts don't start early
|
||||
if (_path->escortTaskID != taskID)
|
||||
continue;
|
||||
|
||||
// search target IDs
|
||||
for (int32_t pID : _path->targetIDs) {
|
||||
if (id == pID) {
|
||||
match = &(*_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match != nullptr)
|
||||
break; // early break for ID matches, since ID has higher priority than type
|
||||
|
||||
// search target types
|
||||
for (int32_t pType : _path->targetTypes) {
|
||||
if (type == pType) {
|
||||
match = &(*_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match != nullptr)
|
||||
break;
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
void Transport::constructPathNPC(int32_t id, NPCPath* path) {
|
||||
BaseNPC* npc = NPCManager::NPCs[id];
|
||||
if (npc->type == EntityType::MOB)
|
||||
((Mob*)(npc))->staticPath = true;
|
||||
npc->loopingPath = path->isLoop;
|
||||
|
||||
// Interpolate
|
||||
std::vector<Vec3> pathPoints = path->points;
|
||||
std::queue<Vec3> points;
|
||||
|
||||
auto _point = pathPoints.begin();
|
||||
Vec3 from = *_point; // point A coords
|
||||
for (_point++; _point != pathPoints.end(); _point++) { // loop through all point Bs
|
||||
Vec3 to = *_point; // point B coords
|
||||
// add point A to the queue
|
||||
if (path->isRelative) {
|
||||
// relative; the NPCs current position is assumed to be its spawn point
|
||||
Vec3 fromReal = { from.x + npc->x, from.y + npc->y, from.z + npc->z };
|
||||
Vec3 toReal = { to.x + npc->x, to.y + npc->y, to.z + npc->z };
|
||||
points.push(fromReal);
|
||||
Transport::lerp(&points, fromReal, toReal, path->speed); // lerp from A to B
|
||||
}
|
||||
else {
|
||||
// absolute
|
||||
points.push(from);
|
||||
Transport::lerp(&points, from, to, path->speed); // lerp from A to B
|
||||
}
|
||||
|
||||
from = to; // update point A
|
||||
}
|
||||
|
||||
Transport::NPCQueues[id] = points;
|
||||
}
|
||||
|
||||
void Transport::init() {
|
||||
REGISTER_SHARD_TIMER(tickTransportationSystem, 1000);
|
||||
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION, transportRegisterLocationHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION, transportWarpHandler);
|
||||
}
|
||||
56
src/Transport.hpp
Normal file
56
src/Transport.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
const int SLIDER_SPEED = 1200;
|
||||
const int SLIDER_STOP_TICKS = 16;
|
||||
const int SLIDER_GAP_SIZE = 45000;
|
||||
|
||||
const int NPC_DEFAULT_SPEED = 300;
|
||||
|
||||
struct Vec3 {
|
||||
int x, y, z;
|
||||
};
|
||||
|
||||
struct WarpLocation {
|
||||
int x, y, z, instanceID, isInstance, limitTaskID, npcID;
|
||||
};
|
||||
|
||||
struct TransportRoute {
|
||||
int type, start, end, cost, mssSpeed, mssRouteNum;
|
||||
};
|
||||
|
||||
struct TransportLocation {
|
||||
int npcID, x, y, z;
|
||||
};
|
||||
|
||||
struct NPCPath {
|
||||
std::vector<Vec3> points;
|
||||
std::vector<int32_t> targetIDs;
|
||||
std::vector<int32_t> targetTypes;
|
||||
int speed;
|
||||
int escortTaskID;
|
||||
bool isRelative;
|
||||
bool isLoop;
|
||||
};
|
||||
|
||||
namespace Transport {
|
||||
extern std::map<int32_t, TransportRoute> Routes;
|
||||
extern std::map<int32_t, TransportLocation> Locations;
|
||||
extern std::vector<NPCPath> NPCPaths; // predefined NPC paths
|
||||
extern std::map<int32_t, std::queue<Vec3>> SkywayPaths; // predefined skyway paths with points
|
||||
extern std::unordered_map<CNSocket*, std::queue<Vec3>> SkywayQueues; // player sockets with queued broomstick points
|
||||
extern std::unordered_map<int32_t, std::queue<Vec3>> NPCQueues; // NPC ids with queued pathing points
|
||||
|
||||
void init();
|
||||
|
||||
void testMssRoute(CNSocket *sock, std::vector<Vec3>* route);
|
||||
|
||||
void lerp(std::queue<Vec3>*, Vec3, Vec3, int, float);
|
||||
void lerp(std::queue<Vec3>*, Vec3, Vec3, int);
|
||||
|
||||
NPCPath* findApplicablePath(int32_t, int32_t, int = -1);
|
||||
void constructPathNPC(int32_t, NPCPath*);
|
||||
}
|
||||
400
src/Vendors.cpp
Normal file
400
src/Vendors.cpp
Normal file
@@ -0,0 +1,400 @@
|
||||
#include "Vendors.hpp"
|
||||
#include "Rand.hpp"
|
||||
|
||||
// 7 days
|
||||
#define VEHICLE_EXPIRY_DURATION 604800
|
||||
|
||||
using namespace Vendors;
|
||||
|
||||
std::map<int32_t, std::vector<VendorListing>> Vendors::VendorTables;
|
||||
|
||||
static void vendorBuy(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_BUY*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// prepare fail packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp);
|
||||
failResp.iErrorCode = 0;
|
||||
|
||||
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end()) {
|
||||
std::cout << "[WARN] Vendor with ID " << req->iVendorID << " mismatched or not found (buy)" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<VendorListing>* listings = &Vendors::VendorTables[req->iVendorID];
|
||||
VendorListing reqItem;
|
||||
reqItem.id = req->Item.iID;
|
||||
reqItem.type = req->Item.iType;
|
||||
reqItem.sort = 0; // just to be safe
|
||||
|
||||
if (std::find(listings->begin(), listings->end(), reqItem) == listings->end()) { // item not found in listing
|
||||
std::cout << "[WARN] Player " << PlayerManager::getPlayerName(plr) << " tried to buy an item that wasn't on sale" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
Items::Item* itemDat = Items::getItemData(req->Item.iID, req->Item.iType);
|
||||
if (itemDat == nullptr) {
|
||||
std::cout << "[WARN] Item id " << req->Item.iID << " with type " << req->Item.iType << " not found (buy)" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
int itemCost = itemDat->buyPrice * (itemDat->stackSize > 1 ? req->Item.iOpt : 1);
|
||||
int slot = Items::findFreeSlot(plr);
|
||||
if (itemCost > plr->money || slot == -1) {
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// crates don't have a stack size in TableData, so we can't check those
|
||||
if (itemDat->stackSize != 0 && req->Item.iOpt > itemDat->stackSize) {
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// if vehicle
|
||||
if (req->Item.iType == 10) {
|
||||
// set time limit: current time + expiry duration
|
||||
req->Item.iTimeLimit = getTimestamp() + VEHICLE_EXPIRY_DURATION;
|
||||
}
|
||||
|
||||
if (slot != req->iInvenSlotNum) {
|
||||
// possible item stacking?
|
||||
std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl;
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC, resp);
|
||||
|
||||
plr->money = plr->money - itemCost;
|
||||
plr->Inven[slot] = req->Item;
|
||||
|
||||
resp.iCandy = plr->money;
|
||||
resp.iInvenSlotNum = slot;
|
||||
resp.Item = req->Item;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC);
|
||||
}
|
||||
|
||||
static void vendorSell(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_SELL*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// prepare a fail packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL, failResp);
|
||||
failResp.iErrorCode = 0;
|
||||
|
||||
if (req->iInvenSlotNum < 0 || req->iInvenSlotNum >= AINVEN_COUNT || req->iItemCnt < 0) {
|
||||
std::cout << "[WARN] Client failed to sell item in slot " << req->iInvenSlotNum << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
sItemBase* item = &plr->Inven[req->iInvenSlotNum];
|
||||
Items::Item* itemData = Items::getItemData(item->iID, item->iType);
|
||||
|
||||
if (itemData == nullptr || !itemData->sellable || item->iOpt < req->iItemCnt) { // sanity + sellable check
|
||||
std::cout << "[WARN] Item id " << item->iID << " with type " << item->iType << " not found (sell)" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// fail to sell croc-potted items
|
||||
if (item->iOpt >= 1 << 16) {
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
sItemBase original;
|
||||
memcpy(&original, item, sizeof(sItemBase));
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, resp);
|
||||
|
||||
// increment taros
|
||||
plr->money += itemData->sellPrice * req->iItemCnt;
|
||||
|
||||
// modify item
|
||||
if (item->iOpt - req->iItemCnt > 0) { // selling part of a stack
|
||||
item->iOpt -= req->iItemCnt;
|
||||
original.iOpt = req->iItemCnt;
|
||||
}
|
||||
else { // selling entire slot
|
||||
// make sure it's fully zeroed, even the padding and non-104 members
|
||||
memset(item, 0, sizeof(*item));
|
||||
}
|
||||
|
||||
// add to buyback list
|
||||
plr->buyback.push_back(original);
|
||||
// forget oldest member if there's more than 5
|
||||
if (plr->buyback.size() > 5)
|
||||
plr->buyback.erase(plr->buyback.begin());
|
||||
//std::cout << (int)plr->buyback.size() << " items in buyback\n";
|
||||
|
||||
// response parameters
|
||||
resp.iInvenSlotNum = req->iInvenSlotNum;
|
||||
resp.iCandy = plr->money;
|
||||
resp.Item = original; // the item that gets sent to buyback
|
||||
resp.ItemStay = *item; // the void item that gets put in the slot
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC);
|
||||
}
|
||||
|
||||
static void vendorBuyback(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// prepare fail packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, failResp);
|
||||
failResp.iErrorCode = 0;
|
||||
|
||||
//std::cout << "buying back from index " << (int)req->iListID << " into " << (int)req->iInvenSlotNum <<
|
||||
// " from " << (int)req->iNPC_ID << " (vendor = " << (int)req->iVendorID << ")\n";
|
||||
|
||||
int idx = req->iListID - 1;
|
||||
|
||||
// sanity check
|
||||
if (idx < 0 || idx >= plr->buyback.size()) {
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// get the item out of the buyback list
|
||||
sItemBase item = plr->buyback[idx];
|
||||
/*
|
||||
* NOTE: The client sends the index of the exact item the user clicked on.
|
||||
* We then operate on that item, but we remove the *first* identical item
|
||||
* from the buyback list, instead of the one at the supplied index.
|
||||
*
|
||||
* This was originally a mistake on my part, but it turns out the client
|
||||
* does the exact same thing, so this *is* the correct thing to do to keep
|
||||
* them in sync.
|
||||
*/
|
||||
for (auto it = plr->buyback.begin(); it != plr->buyback.end(); it++) {
|
||||
/*
|
||||
* XXX: we really need a standard item comparison function that
|
||||
* will work properly across all builds (ex. with iSerial)
|
||||
*/
|
||||
if (it->iType == item.iType && it->iID == item.iID && it->iOpt == item.iOpt
|
||||
&& it->iTimeLimit == item.iTimeLimit) {
|
||||
plr->buyback.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
//std::cout << (int)plr->buyback.size() << " items in buyback\n";
|
||||
|
||||
Items::Item* itemDat = Items::getItemData(item.iID, item.iType);
|
||||
|
||||
if (itemDat == nullptr) {
|
||||
std::cout << "[WARN] Item id " << item.iID << " with type " << item.iType << " not found (rebuy)" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// sell price is used on rebuy. ternary identifies stacked items
|
||||
int itemCost = itemDat->sellPrice * (itemDat->stackSize > 1 ? item.iOpt : 1);
|
||||
int slot = Items::findFreeSlot(plr);
|
||||
if (itemCost > plr->money || slot == -1) {
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot != req->iInvenSlotNum) {
|
||||
// possible item stacking?
|
||||
std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl;
|
||||
}
|
||||
|
||||
plr->money = plr->money - itemCost;
|
||||
plr->Inven[slot] = item;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp);
|
||||
// response parameters
|
||||
resp.iCandy = plr->money;
|
||||
resp.iInvenSlotNum = slot;
|
||||
resp.Item = item;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC);
|
||||
}
|
||||
|
||||
static void vendorTable(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE*)data->buf;
|
||||
|
||||
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end())
|
||||
return;
|
||||
|
||||
std::vector<VendorListing>& listings = Vendors::VendorTables[req->iVendorID];
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, resp);
|
||||
|
||||
for (int i = 0; i < (int)listings.size() && i < 20; i++) { // 20 is the max
|
||||
sItemBase base = {};
|
||||
base.iID = listings[i].id;
|
||||
base.iType = listings[i].type;
|
||||
|
||||
/*
|
||||
* Set vehicle expiry value.
|
||||
*
|
||||
* Note: sItemBase.iTimeLimit in the context of vendor listings contains
|
||||
* a duration, unlike in most other contexts where it contains the
|
||||
* expiration timestamp.
|
||||
*/
|
||||
if (listings[i].type == 10)
|
||||
base.iTimeLimit = VEHICLE_EXPIRY_DURATION;
|
||||
|
||||
sItemVendor vItem;
|
||||
vItem.item = base;
|
||||
vItem.iSortNum = listings[i].sort;
|
||||
vItem.iVendorID = req->iVendorID;
|
||||
//vItem.fBuyCost = listings[i].price; // this value is not actually the one that is used
|
||||
|
||||
resp.item[i] = vItem;
|
||||
}
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC);
|
||||
}
|
||||
|
||||
static void vendorStart(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_START*)data->buf;
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_START_SUCC, resp);
|
||||
|
||||
resp.iNPC_ID = req->iNPC_ID;
|
||||
resp.iVendorID = req->iVendorID;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_START_SUCC);
|
||||
}
|
||||
|
||||
static void vendorBuyBattery(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_VENDOR_BATTERY_BUY*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
int cost = req->Item.iOpt * 100;
|
||||
if ((req->Item.iID == 3 ? (plr->batteryW >= 9999) : (plr->batteryN >= 9999)) || plr->money < cost || req->Item.iOpt < 0) { // sanity check
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp);
|
||||
failResp.iErrorCode = 0;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
cost = plr->batteryW + plr->batteryN;
|
||||
plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 100 : 0;
|
||||
plr->batteryN += req->Item.iID == 4 ? req->Item.iOpt * 100 : 0;
|
||||
|
||||
// caps
|
||||
if (plr->batteryW > 9999)
|
||||
plr->batteryW = 9999;
|
||||
if (plr->batteryN > 9999)
|
||||
plr->batteryN = 9999;
|
||||
|
||||
cost = plr->batteryW + plr->batteryN - cost;
|
||||
plr->money -= cost;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp);
|
||||
|
||||
resp.iCandy = plr->money;
|
||||
resp.iBatteryW = plr->batteryW;
|
||||
resp.iBatteryN = plr->batteryN;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC);
|
||||
}
|
||||
|
||||
static void vendorCombineItems(CNSocket* sock, CNPacketData* data) {
|
||||
auto req = (sP_CL2FE_REQ_PC_ITEM_COMBINATION*)data->buf;
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
|
||||
// prepare fail packet
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_FAIL, failResp);
|
||||
failResp.iCostumeItemSlot = req->iCostumeItemSlot;
|
||||
failResp.iStatItemSlot = req->iStatItemSlot;
|
||||
failResp.iErrorCode = 0;
|
||||
|
||||
// sanity check slot indices
|
||||
if (req->iCostumeItemSlot < 0 || req->iCostumeItemSlot >= AINVEN_COUNT || req->iStatItemSlot < 0 || req->iStatItemSlot >= AINVEN_COUNT) {
|
||||
std::cout << "[WARN] Inventory slot(s) out of range (" << req->iStatItemSlot << " and " << req->iCostumeItemSlot << ")" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
sItemBase* itemStats = &plr->Inven[req->iStatItemSlot];
|
||||
sItemBase* itemLooks = &plr->Inven[req->iCostumeItemSlot];
|
||||
Items::Item* itemStatsDat = Items::getItemData(itemStats->iID, itemStats->iType);
|
||||
Items::Item* itemLooksDat = Items::getItemData(itemLooks->iID, itemLooks->iType);
|
||||
|
||||
// sanity check item and combination entry existence
|
||||
if (itemStatsDat == nullptr || itemLooksDat == nullptr
|
||||
|| Items::CrocPotTable.find(abs(itemStatsDat->level - itemLooksDat->level)) == Items::CrocPotTable.end()) {
|
||||
std::cout << "[WARN] Either item ids or croc pot value set not found" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
// sanity check matching item types
|
||||
if (itemStats->iType != itemLooks->iType
|
||||
|| (itemStats->iType == 0 && itemStatsDat->weaponType != itemLooksDat->weaponType)) {
|
||||
std::cout << "[WARN] Player attempted to combine mismatched items" << std::endl;
|
||||
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
CrocPotEntry* recipe = &Items::CrocPotTable[abs(itemStatsDat->level - itemLooksDat->level)];
|
||||
int cost = itemStatsDat->buyPrice * recipe->multStats + itemLooksDat->buyPrice * recipe->multLooks;
|
||||
float successChance = recipe->base / 100.0f; // base success chance
|
||||
|
||||
// rarity gap multiplier
|
||||
switch (abs(itemStatsDat->rarity - itemLooksDat->rarity)) {
|
||||
case 0:
|
||||
successChance *= recipe->rd0;
|
||||
break;
|
||||
case 1:
|
||||
successChance *= recipe->rd1;
|
||||
break;
|
||||
case 2:
|
||||
successChance *= recipe->rd2;
|
||||
break;
|
||||
case 3:
|
||||
successChance *= recipe->rd3;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
float rolled = Rand::randFloat(100.0f); // success chance out of 100
|
||||
//std::cout << rolled << " vs " << successChance << std::endl;
|
||||
plr->money -= cost;
|
||||
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, resp);
|
||||
if (rolled < successChance) {
|
||||
// success
|
||||
resp.iSuccessFlag = 1;
|
||||
|
||||
// modify the looks item with the new stats and set the appearance through iOpt
|
||||
itemLooks->iOpt = (int32_t)((itemLooks->iOpt) >> 16 > 0 ? (itemLooks->iOpt >> 16) : itemLooks->iID) << 16;
|
||||
itemLooks->iID = itemStats->iID;
|
||||
|
||||
// delete stats item
|
||||
itemStats->iID = 0;
|
||||
itemStats->iOpt = 0;
|
||||
itemStats->iTimeLimit = 0;
|
||||
itemStats->iType = 0;
|
||||
}
|
||||
else {
|
||||
// failure; don't do anything?
|
||||
resp.iSuccessFlag = 0;
|
||||
}
|
||||
resp.iCandy = plr->money;
|
||||
resp.iNewItemSlot = req->iCostumeItemSlot;
|
||||
resp.iStatItemSlot = req->iStatItemSlot;
|
||||
resp.sNewItem = *itemLooks;
|
||||
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_COMBINATION_SUCC);
|
||||
}
|
||||
|
||||
void Vendors::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_START, vendorStart);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE, vendorTable);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_BUY, vendorBuy);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_SELL, vendorSell);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY, vendorBuyback);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY, vendorBuyBattery);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_COMBINATION, vendorCombineItems);
|
||||
}
|
||||
22
src/Vendors.hpp
Normal file
22
src/Vendors.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/Core.hpp"
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "Items.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
|
||||
struct VendorListing {
|
||||
int sort, type, id;
|
||||
|
||||
// when validating a listing, we don't really care about the sorting index
|
||||
bool operator==(const VendorListing& other) const {
|
||||
return type == other.type && id == other.id;
|
||||
}
|
||||
};
|
||||
|
||||
namespace Vendors {
|
||||
extern std::map<int32_t, std::vector<VendorListing>> VendorTables;
|
||||
|
||||
void init();
|
||||
}
|
||||
567
src/core/CNProtocol.cpp
Normal file
567
src/core/CNProtocol.cpp
Normal file
@@ -0,0 +1,567 @@
|
||||
#include "core/CNProtocol.hpp"
|
||||
#include "CNStructs.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
// ========================================================[[ CNSocketEncryption ]]========================================================
|
||||
|
||||
// literally C/P from the client and converted to C++ (does some byte swapping /shrug)
|
||||
int CNSocketEncryption::Encrypt_byte_change_A(int ERSize, uint8_t* data, int size) {
|
||||
int num = 0;
|
||||
int num2 = 0;
|
||||
int num3 = 0;
|
||||
|
||||
while (num + ERSize <= size) {
|
||||
int num4 = num + num3;
|
||||
int num5 = num + (ERSize - 1 - num3);
|
||||
|
||||
uint8_t b = data[num4];
|
||||
data[num4] = data[num5];
|
||||
data[num5] = b;
|
||||
num += ERSize;
|
||||
num3++;
|
||||
if (num3 > ERSize / 2) {
|
||||
num3 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
num2 = ERSize - (num + ERSize - size);
|
||||
return num + num2;
|
||||
}
|
||||
|
||||
int CNSocketEncryption::xorData(uint8_t* buffer, uint8_t* key, int size) {
|
||||
// xor every 8 bytes with 8 byte key
|
||||
for (int i = 0; i < size; i++) {
|
||||
buffer[i] ^= key[i % keyLength];
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
uint64_t CNSocketEncryption::createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2) {
|
||||
uint64_t num = (uint64_t)(iv1 + 1);
|
||||
uint64_t num2 = (uint64_t)(iv2 + 1);
|
||||
uint64_t dEKey;
|
||||
memcpy(&dEKey, defaultKey, sizeof(dEKey));
|
||||
return dEKey * (uTime * num * num2);
|
||||
}
|
||||
|
||||
int CNSocketEncryption::encryptData(uint8_t* buffer, uint8_t* key, int size) {
|
||||
int eRSize = size % (keyLength / 2 + 1) * 2 + keyLength; // C/P from client
|
||||
int size2 = xorData(buffer, key, size);
|
||||
return Encrypt_byte_change_A(eRSize, buffer, size2);
|
||||
}
|
||||
|
||||
int CNSocketEncryption::decryptData(uint8_t* buffer, uint8_t* key, int size) {
|
||||
int eRSize = size % (keyLength / 2 + 1) * 2 + keyLength; // size % of 18????
|
||||
int size2 = Encrypt_byte_change_A(eRSize, buffer, size);
|
||||
return xorData(buffer, key, size2);
|
||||
}
|
||||
|
||||
// ========================================================[[ CNPacketData ]]========================================================
|
||||
|
||||
CNPacketData::CNPacketData(void *b, uint32_t t, int l, int trnum, void *trs):
|
||||
buf(b), size(l), type(t), trCnt(trnum), trailers(trs) {}
|
||||
|
||||
// ========================================================[[ CNSocket ]]========================================================
|
||||
|
||||
CNSocket::CNSocket(SOCKET s, struct sockaddr_in &addr, PacketHandler ph): sock(s), sockaddr(addr), pHandler(ph) {
|
||||
memcpy(&EKey, CNSocketEncryption::defaultKey, sizeof(EKey));
|
||||
}
|
||||
|
||||
bool CNSocket::sendData(uint8_t* data, int size) {
|
||||
int sentBytes = 0;
|
||||
int maxTries = 10;
|
||||
|
||||
while (sentBytes < size) {
|
||||
int sent = send(sock, (buffer_t*)(data + sentBytes), size - sentBytes, 0);
|
||||
if (SOCKETERROR(sent)) {
|
||||
if (OF_ERRNO == OF_EWOULD && maxTries > 0) {
|
||||
maxTries--;
|
||||
continue; // try again
|
||||
}
|
||||
printSocketError("send");
|
||||
return false; // error occured while sending bytes
|
||||
}
|
||||
sentBytes += sent;
|
||||
}
|
||||
|
||||
return true; // it worked!
|
||||
}
|
||||
|
||||
void CNSocket::setEKey(uint64_t k) {
|
||||
EKey = k;
|
||||
}
|
||||
|
||||
void CNSocket::setFEKey(uint64_t k) {
|
||||
FEKey = k;
|
||||
}
|
||||
|
||||
uint64_t CNSocket::getEKey() {
|
||||
return EKey;
|
||||
}
|
||||
|
||||
uint64_t CNSocket::getFEKey() {
|
||||
return FEKey;
|
||||
}
|
||||
|
||||
bool CNSocket::isAlive() {
|
||||
return alive;
|
||||
}
|
||||
|
||||
void CNSocket::kill() {
|
||||
if (!alive)
|
||||
return;
|
||||
|
||||
alive = false;
|
||||
|
||||
#ifdef _WIN32
|
||||
shutdown(sock, SD_BOTH);
|
||||
closesocket(sock);
|
||||
#else
|
||||
shutdown(sock, SHUT_RDWR);
|
||||
close(sock);
|
||||
#endif
|
||||
}
|
||||
|
||||
void CNSocket::validatingSendPacket(void *pkt, uint32_t packetType) {
|
||||
assert(isOutboundPacketID(packetType));
|
||||
assert(Packets::packets.find(packetType) != Packets::packets.end());
|
||||
|
||||
PacketDesc& desc = Packets::packets[packetType];
|
||||
size_t resplen = desc.size;
|
||||
|
||||
/*
|
||||
* Note that this validation doesn't happen on time to prevent a buffer
|
||||
* overflow if it would have taken place, but we do it anyway so the
|
||||
* assertion failure at least makes it clear that something isn't being
|
||||
* validated properly.
|
||||
*/
|
||||
if (desc.variadic) {
|
||||
int32_t ntrailers = *(int32_t*)(((uint8_t*)pkt) + desc.cntMembOfs);
|
||||
assert(validOutVarPacket(desc.size, ntrailers, desc.trailerSize));
|
||||
resplen = desc.size + ntrailers * desc.trailerSize;
|
||||
}
|
||||
|
||||
sendPacket(pkt, packetType, resplen);
|
||||
}
|
||||
|
||||
void CNSocket::sendPacket(void* buf, uint32_t type, size_t size) {
|
||||
if (!alive)
|
||||
return;
|
||||
|
||||
uint8_t fullpkt[CN_PACKET_BUFFER_SIZE]; // length, type, body
|
||||
uint8_t* body = fullpkt + 4; // packet without length (type, body)
|
||||
size_t bodysize = size + 4;
|
||||
|
||||
// set packet length
|
||||
memcpy(fullpkt, (void*)&bodysize, 4);
|
||||
|
||||
// copy packet type to the front of the buffer & then the actual buffer
|
||||
memcpy(body, (void*)&type, 4);
|
||||
memcpy(body+4, buf, size);
|
||||
|
||||
// encrypt the packet
|
||||
switch (activeKey) {
|
||||
case SOCKETKEY_E:
|
||||
CNSocketEncryption::encryptData((uint8_t*)body, (uint8_t*)(&EKey), bodysize);
|
||||
break;
|
||||
case SOCKETKEY_FE:
|
||||
CNSocketEncryption::encryptData((uint8_t*)body, (uint8_t*)(&FEKey), bodysize);
|
||||
break;
|
||||
default:
|
||||
DEBUGLOG(
|
||||
std::cout << "[WARN]: UNSET KEYTYPE FOR SOCKET!! ABORTING SEND" << std::endl;
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
// send packet data!
|
||||
if (alive && !sendData(fullpkt, bodysize+4))
|
||||
kill();
|
||||
}
|
||||
|
||||
void CNSocket::setActiveKey(ACTIVEKEY key) {
|
||||
activeKey = key;
|
||||
}
|
||||
|
||||
inline void CNSocket::parsePacket(uint8_t *buf, size_t size) {
|
||||
uint32_t type = *((uint32_t*)buf);
|
||||
uint8_t *body = buf + 4;
|
||||
size_t pktSize = size - 4;
|
||||
|
||||
if (Packets::packets.find(type) == Packets::packets.end()) {
|
||||
std::cerr << "OpenFusion: UNKNOWN PACKET: " << (int)type << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInboundPacketID(type)) {
|
||||
std::cerr << "OpenFusion: UNEXPECTED PACKET: " << (int)type << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
PacketDesc& desc = Packets::packets[type];
|
||||
|
||||
/*
|
||||
* Some packet structs with no meaningful contents have length 1, but
|
||||
* the client doesn't transmit that byte at all, so we special-case that.
|
||||
* It's important that we do that by zeroing that byte, as the server could
|
||||
* hypothetically try and read from it and get a byte of the previous
|
||||
* packet's contents.
|
||||
*
|
||||
* Assigning a zero byte to the body like this is safe, since there's a
|
||||
* huge empty buffer behind that pointer.
|
||||
*/
|
||||
if (!desc.variadic && desc.size == 1 && pktSize == 0) {
|
||||
pktSize = 1;
|
||||
*body = 0;
|
||||
}
|
||||
|
||||
int32_t ntrailers = 0;
|
||||
if (desc.variadic) {
|
||||
ntrailers = *(int32_t*)(body + desc.cntMembOfs);
|
||||
if (!validInVarPacket(desc.size, ntrailers, desc.trailerSize, pktSize)) {
|
||||
std::cerr << "[WARN] Received invalid variadic packet: " << desc.name << " (" << type << ")" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (!desc.variadic && pktSize != desc.size) {
|
||||
std::cerr << "[WARN] Received " << desc.name << " (" << type << ") of wrong size ("
|
||||
<< (int)pktSize << " vs " << desc.size << ")" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
void *trailers = nullptr;
|
||||
if (desc.variadic)
|
||||
trailers = body + desc.size;
|
||||
|
||||
CNPacketData pkt(body, type, pktSize, ntrailers, trailers);
|
||||
pHandler(this, &pkt);
|
||||
}
|
||||
|
||||
void CNSocket::step() {
|
||||
// read step
|
||||
|
||||
// XXX NOTE: we must not recv() twice without a poll() inbetween
|
||||
if (readSize <= 0) {
|
||||
// we aren't reading a packet yet, try to start looking for one
|
||||
int recved = recv(sock, (buffer_t*)readBuffer, sizeof(int32_t), 0);
|
||||
if (recved >= 0 && recved < sizeof(int32_t)) {
|
||||
// too little data for readSize or the socket was closed normally (when 0 bytes were read)
|
||||
kill();
|
||||
return;
|
||||
} else if (!SOCKETERROR(recved)) {
|
||||
// we got our packet size!!!!
|
||||
readSize = *((int32_t*)readBuffer);
|
||||
// sanity check
|
||||
if (readSize > CN_PACKET_BUFFER_SIZE) {
|
||||
kill();
|
||||
return;
|
||||
}
|
||||
|
||||
// we'll just leave bufferIndex at 0 since we already have the packet size, it's safe to overwrite those bytes
|
||||
activelyReading = true;
|
||||
} else if (OF_ERRNO != OF_EWOULD) {
|
||||
// serious socket issue, disconnect connection
|
||||
printSocketError("recv");
|
||||
kill();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (readSize > 0 && readBufferIndex < readSize) {
|
||||
// read until the end of the packet (or at least try to)
|
||||
int recved = recv(sock, (buffer_t*)(readBuffer + readBufferIndex), readSize - readBufferIndex, 0);
|
||||
if (recved == 0) {
|
||||
// the socket was closed normally
|
||||
kill();
|
||||
return;
|
||||
} else if (!SOCKETERROR(recved))
|
||||
readBufferIndex += recved;
|
||||
else if (OF_ERRNO != OF_EWOULD) {
|
||||
// serious socket issue, disconnect connection
|
||||
printSocketError("recv");
|
||||
kill();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (activelyReading && readBufferIndex >= readSize) {
|
||||
// decrypt readBuffer and copy to CNPacketData
|
||||
CNSocketEncryption::decryptData((uint8_t*)&readBuffer, (uint8_t*)(&EKey), readSize);
|
||||
|
||||
parsePacket(readBuffer, readSize);
|
||||
|
||||
// reset vars :)
|
||||
readSize = 0;
|
||||
readBufferIndex = 0;
|
||||
activelyReading = false;
|
||||
}
|
||||
}
|
||||
|
||||
void printSocketError(const char *call) {
|
||||
#ifdef _WIN32
|
||||
std::cerr << call << ": ";
|
||||
|
||||
LPSTR lpMsgBuf = nullptr; // string buffer
|
||||
DWORD errCode = WSAGetLastError(); // error code
|
||||
|
||||
if (errCode == 0) {
|
||||
std::cerr << "no error code" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bufSize = FormatMessageA( // actually get the error message
|
||||
FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
||||
FORMAT_MESSAGE_FROM_SYSTEM |
|
||||
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
NULL,
|
||||
errCode, // in
|
||||
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||
(LPTSTR)&lpMsgBuf, // out
|
||||
0, NULL);
|
||||
|
||||
// convert buffer to string and output message to terminal
|
||||
std::string msg(lpMsgBuf, bufSize);
|
||||
std::cerr << msg; // newline included
|
||||
|
||||
LocalFree(lpMsgBuf); // free the buffer
|
||||
#else
|
||||
perror(call);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool setSockNonblocking(SOCKET listener, SOCKET newSock) {
|
||||
#ifdef _WIN32
|
||||
unsigned long mode = 1;
|
||||
if (ioctlsocket(newSock, FIONBIO, &mode) != 0) {
|
||||
#else
|
||||
if (fcntl(newSock, F_SETFL, (fcntl(newSock, F_GETFL, 0) | O_NONBLOCK)) != 0) {
|
||||
#endif
|
||||
printSocketError("fcntl");
|
||||
std::cerr << "[WARN] OpenFusion: fcntl failed on new connection" << std::endl;
|
||||
#ifdef _WIN32
|
||||
shutdown(newSock, SD_BOTH);
|
||||
closesocket(newSock);
|
||||
#else
|
||||
shutdown(newSock, SHUT_RDWR);
|
||||
close(newSock);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========================================================[[ CNServer ]]========================================================
|
||||
|
||||
void CNServer::init() {
|
||||
// create socket file descriptor
|
||||
sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (SOCKETINVALID(sock)) {
|
||||
printSocketError("socket");
|
||||
std::cerr << "[FATAL] OpenFusion: socket failed" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// attach socket to the port
|
||||
int opt = 1;
|
||||
#ifdef _WIN32
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)) != 0) {
|
||||
#else
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) {
|
||||
#endif
|
||||
std::cerr << "[FATAL] OpenFusion: setsockopt failed" << std::endl;
|
||||
printSocketError("setsockopt");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
address.sin_port = htons(port);
|
||||
|
||||
addressSize = sizeof(address);
|
||||
|
||||
// Bind to the port
|
||||
if (SOCKETERROR(bind(sock, (struct sockaddr *)&address, addressSize))) {
|
||||
std::cerr << "[FATAL] OpenFusion: bind failed" << std::endl;
|
||||
printSocketError("bind");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (SOCKETERROR(listen(sock, SOMAXCONN))) {
|
||||
std::cerr << "[FATAL] OpenFusion: listen failed" << std::endl;
|
||||
printSocketError("listen");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// set server listener to non-blocking
|
||||
#ifdef _WIN32
|
||||
unsigned long mode = 1;
|
||||
if (ioctlsocket(sock, FIONBIO, &mode) != 0) {
|
||||
#else
|
||||
if (fcntl(sock, F_SETFL, (fcntl(sock, F_GETFL, 0) | O_NONBLOCK)) != 0) {
|
||||
#endif
|
||||
printSocketError("fcntl");
|
||||
std::cerr << "[FATAL] OpenFusion: fcntl failed" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// poll() configuration
|
||||
fds.reserve(STARTFDSCOUNT);
|
||||
fds.push_back({sock, POLLIN});
|
||||
}
|
||||
|
||||
CNServer::CNServer() {};
|
||||
CNServer::CNServer(uint16_t p): port(p) {}
|
||||
|
||||
void CNServer::addPollFD(SOCKET s) {
|
||||
fds.push_back({s, POLLIN});
|
||||
}
|
||||
|
||||
void CNServer::removePollFD(int fd) {
|
||||
auto it = fds.begin();
|
||||
while (it != fds.end() && it->fd != fd)
|
||||
it++;
|
||||
assert(it != fds.end());
|
||||
|
||||
fds.erase(it);
|
||||
}
|
||||
|
||||
void CNServer::start() {
|
||||
std::cout << "Starting server at *:" << port << std::endl;
|
||||
while (active) {
|
||||
// the timeout is to ensure shard timers are ticking
|
||||
int n = poll(fds.data(), fds.size(), 50);
|
||||
if (SOCKETERROR(n)) {
|
||||
#ifndef _WIN32
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
#endif
|
||||
std::cout << "[FATAL] poll() returned error" << std::endl;
|
||||
printSocketError("poll");
|
||||
terminate(0);
|
||||
}
|
||||
|
||||
for (int i = 0; i < fds.size() && n > 0; i++) {
|
||||
if (fds[i].revents == 0)
|
||||
continue; // nothing in this one; don't decrement n
|
||||
|
||||
n--;
|
||||
|
||||
// is it the listener?
|
||||
if (fds[i].fd == sock) {
|
||||
// any sort of error on the listener
|
||||
if (fds[i].revents & ~POLLIN) {
|
||||
std::cout << "[FATAL] Error on listener socket" << std::endl;
|
||||
terminate(0);
|
||||
}
|
||||
|
||||
SOCKET newConnectionSocket = accept(sock, (struct sockaddr *)&address, (socklen_t*)&addressSize);
|
||||
if (SOCKETINVALID(newConnectionSocket)) {
|
||||
printSocketError("accept");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!setSockNonblocking(sock, newConnectionSocket))
|
||||
continue;
|
||||
|
||||
std::cout << "New " << serverType << " connection! " << inet_ntoa(address.sin_addr) << std::endl;
|
||||
|
||||
addPollFD(newConnectionSocket);
|
||||
|
||||
// add connection to list!
|
||||
CNSocket* tmp = new CNSocket(newConnectionSocket, address, pHandler);
|
||||
connections[newConnectionSocket] = tmp;
|
||||
newConnection(tmp);
|
||||
|
||||
} else if (checkExtraSockets(i)) {
|
||||
// no-op. handled in checkExtraSockets().
|
||||
|
||||
} else {
|
||||
std::lock_guard<std::mutex> lock(activeCrit); // protect operations on connections
|
||||
|
||||
// halt packet handling if server is shutting down
|
||||
if (!active)
|
||||
return;
|
||||
|
||||
// player sockets
|
||||
if (connections.find(fds[i].fd) == connections.end()) {
|
||||
std::cout << "[FATAL] Event on non-existent socket: " << fds[i].fd << std::endl;
|
||||
assert(0);
|
||||
/* not reached */
|
||||
}
|
||||
|
||||
CNSocket* cSock = connections[fds[i].fd];
|
||||
|
||||
// kill the socket on hangup/error
|
||||
if (fds[i].revents & ~POLLIN)
|
||||
cSock->kill();
|
||||
|
||||
if (cSock->isAlive())
|
||||
cSock->step();
|
||||
}
|
||||
}
|
||||
|
||||
onStep();
|
||||
|
||||
// clean up dead connection sockets
|
||||
auto it = connections.begin();
|
||||
while (it != connections.end()) {
|
||||
CNSocket *cSock = it->second;
|
||||
|
||||
if (!cSock->isAlive()) {
|
||||
killConnection(cSock);
|
||||
it = connections.erase(it);
|
||||
|
||||
removePollFD(cSock->sock);
|
||||
|
||||
delete cSock;
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CNServer::kill() {
|
||||
std::lock_guard<std::mutex> lock(activeCrit); // the lock will be removed when the function ends
|
||||
active = false;
|
||||
|
||||
// kill all connections
|
||||
for (auto& pair : connections) {
|
||||
CNSocket *cSock = pair.second;
|
||||
if (cSock->isAlive())
|
||||
cSock->kill();
|
||||
|
||||
delete cSock;
|
||||
}
|
||||
|
||||
connections.clear();
|
||||
}
|
||||
|
||||
void CNServer::printPacket(CNPacketData *data) {
|
||||
if (settings::VERBOSITY < 2)
|
||||
return;
|
||||
|
||||
if (settings::VERBOSITY < 3) switch (data->type) {
|
||||
case P_CL2LS_REP_LIVE_CHECK:
|
||||
case P_CL2FE_REP_LIVE_CHECK:
|
||||
case P_CL2FE_REQ_PC_MOVE:
|
||||
case P_CL2FE_REQ_PC_JUMP:
|
||||
case P_CL2FE_REQ_PC_SLOPE:
|
||||
case P_CL2FE_REQ_PC_MOVEPLATFORM:
|
||||
case P_CL2FE_REQ_PC_MOVETRANSPORTATION:
|
||||
case P_CL2FE_REQ_PC_ZIPLINE:
|
||||
case P_CL2FE_REQ_PC_JUMPPAD:
|
||||
case P_CL2FE_REQ_PC_LAUNCHER:
|
||||
case P_CL2FE_REQ_PC_STOP:
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "OpenFusion: received " << Packets::p2str(data->type) << " (" << data->type << ")" << std::endl;
|
||||
}
|
||||
|
||||
bool CNServer::checkExtraSockets(int i) { return false; } // stubbed
|
||||
void CNServer::newConnection(CNSocket* cns) {} // stubbed
|
||||
void CNServer::killConnection(CNSocket* cns) {} // stubbed
|
||||
void CNServer::onStep() {} // stubbed
|
||||
258
src/core/CNProtocol.hpp
Normal file
258
src/core/CNProtocol.hpp
Normal file
@@ -0,0 +1,258 @@
|
||||
#pragma once
|
||||
|
||||
#define DEBUGLOG(x) if (settings::VERBOSITY) {x};
|
||||
|
||||
#include <iostream>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#ifdef _WIN32
|
||||
// windows
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#define M_PI 3.14159265358979323846
|
||||
#define _WINSOCK_DEPRECATED_NO_WARNINGS
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <ws2tcpip.h>
|
||||
#pragma comment(lib, "Ws2_32.lib")
|
||||
|
||||
typedef char buffer_t;
|
||||
#define PollFD WSAPOLLFD
|
||||
#define poll WSAPoll
|
||||
#define OF_ERRNO WSAGetLastError()
|
||||
#define OF_EWOULD WSAEWOULDBLOCK
|
||||
#define SOCKETINVALID(x) (x == INVALID_SOCKET)
|
||||
#define SOCKETERROR(x) (x == SOCKET_ERROR)
|
||||
#else
|
||||
// posix platform
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
typedef int SOCKET;
|
||||
typedef void buffer_t;
|
||||
#define PollFD struct pollfd
|
||||
#define OF_ERRNO errno
|
||||
#define OF_EWOULD EWOULDBLOCK
|
||||
#define SOCKETINVALID(x) (x < 0)
|
||||
#define SOCKETERROR(x) (x == -1)
|
||||
#endif
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <csignal>
|
||||
#include <list>
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
#include "Defines.hpp"
|
||||
#include "Packets.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
#if defined(__MINGW32__) && !defined(_GLIBCXX_HAS_GTHREADS)
|
||||
#include "mingw/mingw.mutex.h"
|
||||
#else
|
||||
#include <mutex>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Packets format (sent from the client):
|
||||
* [4 bytes] - size of packet including the 4 byte packet type
|
||||
* [size bytes] - Encrypted packet (byte swapped && xor'd with 8 byte key; see CNSocketEncryption)
|
||||
* [4 bytes] - packet type (which is a combination of the first 4 bytes of the packet and a checksum in some versions)
|
||||
* [structure] - one member contains length of trailing data (expressed in packet-dependant structures)
|
||||
* [trailing data] - optional variable-length data that only some packets make use of
|
||||
*/
|
||||
|
||||
// error checking calloc wrapper
|
||||
inline void* xmalloc(size_t sz) {
|
||||
void* res = calloc(1, sz);
|
||||
|
||||
if (res == NULL) {
|
||||
std::cerr << "[FATAL] OpenFusion: out of memory!" << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
inline constexpr bool isInboundPacketID(uint32_t id) {
|
||||
return ((id & CL2LS) == CL2LS) || ((id & CL2FE) == CL2FE);
|
||||
}
|
||||
|
||||
inline constexpr bool isOutboundPacketID(uint32_t id) {
|
||||
return ((id & LS2CL) == LS2CL) || ((id & FE2CL) == FE2CL);
|
||||
}
|
||||
|
||||
// overflow-safe validation of variable-length packets
|
||||
// for outbound packets
|
||||
inline constexpr bool validOutVarPacket(size_t base, int32_t npayloads, size_t plsize) {
|
||||
// check for multiplication overflow
|
||||
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
|
||||
return false;
|
||||
|
||||
// it's safe to multiply
|
||||
size_t trailing = npayloads * plsize;
|
||||
|
||||
// does it fit in a packet?
|
||||
if (base + trailing > CN_PACKET_BUFFER_SIZE - 8)
|
||||
return false;
|
||||
|
||||
// everything is a-ok!
|
||||
return true;
|
||||
}
|
||||
|
||||
// for inbound packets
|
||||
inline constexpr bool validInVarPacket(size_t base, int32_t npayloads, size_t plsize, size_t datasize) {
|
||||
// check for multiplication overflow
|
||||
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
|
||||
return false;
|
||||
|
||||
// it's safe to multiply
|
||||
size_t trailing = npayloads * plsize;
|
||||
|
||||
// make sure size is exact
|
||||
// datasize has already been validated against CN_PACKET_BUFFER_SIZE
|
||||
if (datasize != base + trailing)
|
||||
return false;
|
||||
|
||||
// everything is a-ok!
|
||||
return true;
|
||||
}
|
||||
|
||||
void printSocketError(const char *func);
|
||||
bool setSockNonblocking(SOCKET listener, SOCKET newSock);
|
||||
|
||||
namespace CNSocketEncryption {
|
||||
// you won't believe how complicated they made it in the client :facepalm:
|
||||
static constexpr const char* defaultKey = "m@rQn~W#";
|
||||
static const unsigned int keyLength = 8;
|
||||
|
||||
int Encrypt_byte_change_A(int ERSize, uint8_t* data, int size);
|
||||
int xorData(uint8_t* buffer, uint8_t* key, int size);
|
||||
uint64_t createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2);
|
||||
int encryptData(uint8_t* buffer, uint8_t* key, int size);
|
||||
int decryptData(uint8_t* buffer, uint8_t* key, int size);
|
||||
}
|
||||
|
||||
struct CNPacketData {
|
||||
void *buf;
|
||||
int size;
|
||||
uint32_t type;
|
||||
int trCnt;
|
||||
void *trailers;
|
||||
|
||||
CNPacketData(void* b, uint32_t t, int l, int trnum, void *trs);
|
||||
};
|
||||
|
||||
enum ACTIVEKEY {
|
||||
SOCKETKEY_E,
|
||||
SOCKETKEY_FE
|
||||
};
|
||||
|
||||
class CNSocket;
|
||||
typedef void (*PacketHandler)(CNSocket* sock, CNPacketData* data);
|
||||
|
||||
class CNSocket {
|
||||
private:
|
||||
uint64_t EKey;
|
||||
uint64_t FEKey;
|
||||
int32_t readSize = 0;
|
||||
uint8_t readBuffer[CN_PACKET_BUFFER_SIZE];
|
||||
int readBufferIndex = 0;
|
||||
bool activelyReading = false;
|
||||
bool alive = true;
|
||||
|
||||
ACTIVEKEY activeKey;
|
||||
|
||||
bool sendData(uint8_t* data, int size);
|
||||
int recvData(buffer_t* data, int size);
|
||||
|
||||
inline void parsePacket(uint8_t *buf, size_t size);
|
||||
void validatingSendPacket(void *buf, uint32_t packetType);
|
||||
|
||||
public:
|
||||
SOCKET sock;
|
||||
sockaddr_in sockaddr;
|
||||
PacketHandler pHandler;
|
||||
|
||||
CNSocket(SOCKET s, struct sockaddr_in &addr, PacketHandler ph);
|
||||
|
||||
void setEKey(uint64_t k);
|
||||
void setFEKey(uint64_t k);
|
||||
uint64_t getEKey();
|
||||
uint64_t getFEKey();
|
||||
void setActiveKey(ACTIVEKEY t);
|
||||
|
||||
void kill();
|
||||
void sendPacket(void* buf, uint32_t packetType, size_t size);
|
||||
void step();
|
||||
bool isAlive();
|
||||
|
||||
// generic, validating wrapper for sendPacket()
|
||||
template<class T>
|
||||
inline void sendPacket(T& pkt, uint32_t packetType) {
|
||||
/*
|
||||
* We do most of the logic in a helper, to lower the amount of code
|
||||
* that gets generated multiple times with each template instantiation.
|
||||
*/
|
||||
validatingSendPacket((void*)&pkt, packetType);
|
||||
}
|
||||
};
|
||||
|
||||
class CNServer;
|
||||
typedef void (*TimerHandler)(CNServer* serv, time_t time);
|
||||
|
||||
// timer struct
|
||||
struct TimerEvent {
|
||||
TimerHandler handlr;
|
||||
time_t delta; // time to be added to the current time on reset
|
||||
time_t scheduledEvent; // time to call handlr()
|
||||
|
||||
TimerEvent(TimerHandler h, time_t d): handlr(h), delta(d) {
|
||||
scheduledEvent = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// in charge of accepting new connections and making sure each connection is kept alive
|
||||
class CNServer {
|
||||
protected:
|
||||
std::unordered_map<SOCKET, CNSocket*> connections;
|
||||
std::mutex activeCrit;
|
||||
|
||||
const size_t STARTFDSCOUNT = 8; // number of initial PollFD slots
|
||||
std::vector<PollFD> fds;
|
||||
|
||||
std::string serverType = "invalid";
|
||||
SOCKET sock;
|
||||
uint16_t port;
|
||||
socklen_t addressSize;
|
||||
struct sockaddr_in address;
|
||||
void init();
|
||||
|
||||
bool active = true;
|
||||
|
||||
void addPollFD(SOCKET s);
|
||||
void removePollFD(int i);
|
||||
|
||||
public:
|
||||
PacketHandler pHandler;
|
||||
|
||||
CNServer();
|
||||
CNServer(uint16_t p);
|
||||
|
||||
void start();
|
||||
void kill();
|
||||
static void printPacket(CNPacketData *data);
|
||||
virtual bool checkExtraSockets(int i);
|
||||
virtual void newConnection(CNSocket* cns);
|
||||
virtual void killConnection(CNSocket* cns);
|
||||
virtual void onStep();
|
||||
};
|
||||
45
src/core/CNShared.cpp
Normal file
45
src/core/CNShared.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "core/CNShared.hpp"
|
||||
|
||||
static std::unordered_map<int64_t, LoginMetadata*> logins;
|
||||
static std::mutex mtx;
|
||||
|
||||
void CNShared::storeLoginMetadata(int64_t sk, LoginMetadata *lm) {
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
|
||||
// take ownership of connection data
|
||||
logins[sk] = lm;
|
||||
}
|
||||
|
||||
LoginMetadata* CNShared::getLoginMetadata(int64_t sk) {
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
|
||||
// fail if the key isn't found
|
||||
if (logins.find(sk) == logins.end())
|
||||
return nullptr;
|
||||
|
||||
// transfer ownership of connection data to shard
|
||||
LoginMetadata *lm = logins[sk];
|
||||
logins.erase(sk);
|
||||
|
||||
return lm;
|
||||
}
|
||||
|
||||
void CNShared::pruneLoginMetadata(CNServer *serv, time_t currTime) {
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
|
||||
auto it = logins.begin();
|
||||
while (it != logins.end()) {
|
||||
auto& sk = it->first;
|
||||
auto& lm = it->second;
|
||||
|
||||
if (currTime > lm->timestamp + CNSHARED_TIMEOUT) {
|
||||
std::cout << "[WARN] Pruning hung connection attempt" << std::endl;
|
||||
|
||||
// deallocate object and remove map entry
|
||||
delete logins[sk];
|
||||
it = logins.erase(it); // skip the invalidated iterator
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/core/CNShared.hpp
Normal file
29
src/core/CNShared.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* core/CNShared.hpp
|
||||
* There's some data shared between the Login Server and the Shard Server. Of course all of this needs to be thread-safe. No mucking about on this one!
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "Player.hpp"
|
||||
|
||||
/*
|
||||
* Connecions time out after 5 minutes, checked every 30 seconds.
|
||||
*/
|
||||
#define CNSHARED_TIMEOUT 300000
|
||||
#define CNSHARED_PERIOD 30000
|
||||
|
||||
struct LoginMetadata {
|
||||
uint64_t FEKey;
|
||||
int32_t playerId;
|
||||
time_t timestamp;
|
||||
};
|
||||
|
||||
namespace CNShared {
|
||||
void storeLoginMetadata(int64_t sk, LoginMetadata *lm);
|
||||
LoginMetadata* getLoginMetadata(int64_t sk);
|
||||
void pruneLoginMetadata(CNServer *serv, time_t currTime);
|
||||
}
|
||||
65
src/core/CNStructs.hpp
Normal file
65
src/core/CNStructs.hpp
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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));
|
||||
|
||||
#define INITVARPACKET(_buf, _Pkt, _pkt, _Trailer, _trailer) uint8_t _buf[CN_PACKET_BUFFER_SIZE]; \
|
||||
memset(&_buf, 0, CN_PACKET_BUFFER_SIZE); \
|
||||
auto _pkt = (_Pkt*)_buf; \
|
||||
auto _trailer = (_Trailer*)(_pkt + 1);
|
||||
|
||||
// macros to extract fields from instanceIDs
|
||||
#define MAPNUM(x) ((x) & 0xffffffff)
|
||||
#define PLAYERID(x) ((x) >> 32)
|
||||
|
||||
// wrapper for U16toU8
|
||||
#define ARRLEN(x) (sizeof(x)/sizeof(*x))
|
||||
#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))
|
||||
|
||||
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt
|
||||
|
||||
std::string U16toU8(char16_t* src, size_t max);
|
||||
size_t U8toU16(std::string src, char16_t* des, size_t max); // returns number of char16_t that was written at des
|
||||
time_t getTime();
|
||||
time_t getTimestamp();
|
||||
void terminate(int);
|
||||
|
||||
// The PROTOCOL_VERSION definition is defined by the build system.
|
||||
#if !defined(PROTOCOL_VERSION)
|
||||
#include "structs/0104.hpp"
|
||||
#elif PROTOCOL_VERSION == 728
|
||||
#include "structs/0728.hpp"
|
||||
#elif PROTOCOL_VERSION == 104
|
||||
#include "structs/0104.hpp"
|
||||
#elif PROTOCOL_VERSION == 1013
|
||||
#include "structs/1013.hpp"
|
||||
#else
|
||||
#error Invalid PROTOCOL_VERSION
|
||||
#endif
|
||||
|
||||
sSYSTEMTIME timeStampToStruct(uint64_t time);
|
||||
11
src/core/Core.hpp
Normal file
11
src/core/Core.hpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* Convenience header.
|
||||
*
|
||||
* We omit CNShared, as it's only relevant to the Login and Shard servers
|
||||
* and the PlayerManager. We omit Defines, as CNProtocol already includes it.
|
||||
*/
|
||||
|
||||
#include "core/CNProtocol.hpp"
|
||||
#include "core/CNStructs.hpp"
|
||||
934
src/core/Defines.hpp
Normal file
934
src/core/Defines.hpp
Normal file
@@ -0,0 +1,934 @@
|
||||
/* enum definitions from the client */
|
||||
#pragma once
|
||||
|
||||
// floats
|
||||
const float VALUE_BATTERY_EMPTY_PENALTY = 0.5f;
|
||||
const float CN_EP_RANK_1 = 0.8f;
|
||||
const float CN_EP_RANK_2 = 0.7f;
|
||||
const float CN_EP_RANK_3 = 0.5f;
|
||||
const float CN_EP_RANK_4 = 0.3f;
|
||||
const float CN_EP_RANK_5 = 0.29f;
|
||||
|
||||
// methods of finding players for GM commands
|
||||
enum eCN_GM_TargetSearchBy {
|
||||
eCN_GM_TargetSearchBy__PC_ID, // player id
|
||||
eCN_GM_TargetSearchBy__PC_Name, // firstname, lastname
|
||||
eCN_GM_TargetSearchBy__PC_UID // account id
|
||||
};
|
||||
|
||||
enum eCN_GM_TeleportType {
|
||||
eCN_GM_TeleportMapType__XYZ,
|
||||
eCN_GM_TeleportMapType__MapXYZ,
|
||||
eCN_GM_TeleportMapType__MyLocation,
|
||||
eCN_GM_TeleportMapType__SomeoneLocation,
|
||||
eCN_GM_TeleportMapType__Unstick
|
||||
};
|
||||
|
||||
// nano powers
|
||||
enum {
|
||||
EST_NONE = 0,
|
||||
EST_DAMAGE = 1,
|
||||
EST_HEAL_HP = 2,
|
||||
EST_KNOCKDOWN = 3,
|
||||
EST_SLEEP = 4,
|
||||
EST_SNARE = 5,
|
||||
EST_HEAL_STAMINA = 6,
|
||||
EST_STAMINA_SELF = 7,
|
||||
EST_STUN = 8,
|
||||
EST_WEAPONSLOW = 9,
|
||||
EST_JUMP = 10,
|
||||
EST_RUN = 11,
|
||||
EST_STEALTH = 12,
|
||||
EST_SWIM = 13,
|
||||
EST_MINIMAPENEMY = 14,
|
||||
EST_MINIMAPTRESURE = 15,
|
||||
EST_PHOENIX = 16,
|
||||
EST_PROTECTBATTERY = 17,
|
||||
EST_PROTECTINFECTION = 18,
|
||||
EST_REWARDBLOB = 19,
|
||||
EST_REWARDCASH = 20,
|
||||
EST_BATTERYDRAIN = 21,
|
||||
EST_CORRUPTIONATTACK = 22,
|
||||
EST_INFECTIONDAMAGE = 23,
|
||||
EST_KNOCKBACK = 24,
|
||||
EST_FREEDOM = 25,
|
||||
EST_PHOENIX_GROUP = 26,
|
||||
EST_RECALL = 27,
|
||||
EST_RECALL_GROUP = 28,
|
||||
EST_RETROROCKET_SELF = 29,
|
||||
EST_BLOODSUCKING = 30,
|
||||
EST_BOUNDINGBALL = 31,
|
||||
EST_INVULNERABLE = 32,
|
||||
EST_NANOSTIMPAK = 33,
|
||||
EST_RETURNHOMEHEAL = 34,
|
||||
EST_BUFFHEAL = 35,
|
||||
EST_EXTRABANK = 36,
|
||||
EST__END = 37,
|
||||
EST_CORRUPTIONATTACKWIN = 38,
|
||||
EST_CORRUPTIONATTACKLOSE = 39,
|
||||
|
||||
ECSB_NONE = 0,
|
||||
ECSB_UP_MOVE_SPEED = 1,
|
||||
ECSB_UP_SWIM_SPEED = 2,
|
||||
ECSB_UP_JUMP_HEIGHT = 3,
|
||||
ECSB_UP_STEALTH = 4,
|
||||
ECSB_PHOENIX = 5,
|
||||
ECSB_PROTECT_BATTERY = 6,
|
||||
ECSB_PROTECT_INFECTION = 7,
|
||||
ECSB_DN_MOVE_SPEED = 8,
|
||||
ECSB_DN_ATTACK_SPEED = 9,
|
||||
ECSB_STUN = 10,
|
||||
ECSB_MEZ = 11,
|
||||
ECSB_KNOCKDOWN = 12,
|
||||
ECSB_MINIMAP_ENEMY = 13,
|
||||
ECSB_MINIMAP_TRESURE = 14,
|
||||
ECSB_REWARD_BLOB = 15,
|
||||
ECSB_REWARD_CASH = 16,
|
||||
ECSB_INFECTION = 17,
|
||||
ECSB_FREEDOM = 18,
|
||||
ECSB_BOUNDINGBALL = 19,
|
||||
ECSB_INVULNERABLE = 20,
|
||||
ECSB_STIMPAKSLOT1 = 21,
|
||||
ECSB_STIMPAKSLOT2 = 22,
|
||||
ECSB_STIMPAKSLOT3 = 23,
|
||||
ECSB_HEAL = 24,
|
||||
ECSB_EXTRABANK = 25,
|
||||
ECSTB__END = 26,
|
||||
};
|
||||
|
||||
enum {
|
||||
SUCC = 1,
|
||||
FAIL = 0,
|
||||
|
||||
SIZEOF_BYTE = 1,
|
||||
SIZEOF_DWORD = 4,
|
||||
SIZEOF_INT = 4,
|
||||
SIZEOF_FLOAT = 4,
|
||||
SIZEOF_SHORT = 2,
|
||||
SIZEOF_ULONG = 4,
|
||||
SIZEOF_UINT64 = 8,
|
||||
SIZEOF_IP_STRING = 16,
|
||||
SIZEOF_CN_UID_STRING = 50,
|
||||
SIZEOF_ACCOUNT_STRING = 33,
|
||||
SIZEOF_PASSWORD_STRING = 33,
|
||||
SIZEOF_AUTH_ID_STRING = 255,
|
||||
|
||||
CN_MAX_COUNT_GROUP_MEMBER = 5,
|
||||
CN_MAX_COUNT_PC_GROUP_MEMBER = 4,
|
||||
CN_MAX_COUNT_NPC_GROUP_MEMBER = 5,
|
||||
|
||||
CHAT_MAX_STRING = 128,
|
||||
PC_START_LOCATION_RANDOM_RANGE = 10000,
|
||||
SIZEOF_ANNOUNCE_STRING = 512,
|
||||
SERVER_COUNT_SHARD_CLIENT = 25,
|
||||
|
||||
EXIT_CODE_DISCONNECT = 0,
|
||||
EXIT_CODE_REQ_BY_PC = 1,
|
||||
EXIT_CODE_REQ_BY_SVR = 2,
|
||||
EXIT_CODE_REQ_BY_GM = 3,
|
||||
EXIT_CODE_HACK = 4,
|
||||
EXIT_CODE_ERROR = 5,
|
||||
EXIT_CODE_LIVE_CHECK = 6,
|
||||
EXIT_CODE_REQ_BY_PC_DUPE_LOGIN = 7,
|
||||
EXIT_CODE_SERVER_ERROR = 99,
|
||||
|
||||
SIZEOF_USER_ID = 32,
|
||||
SIZEOF_USER_PW = 32,
|
||||
SIZEOF_PC_SLOT = 4,
|
||||
SIZEOF_PC_NAME = 16,
|
||||
SIZEOF_PC_FIRST_NAME = 9,
|
||||
SIZEOF_PC_LAST_NAME = 17,
|
||||
SIZEOF_PC_NAME_FLAG = 8,
|
||||
|
||||
GENDER_NONE = 0,
|
||||
GENDER_MALE = 1,
|
||||
GENDER_FEMALE = 2,
|
||||
|
||||
MENTOR_CHANGE_BASE_COST = 100,
|
||||
REPEAT_MISSION_RESET_TIME = 9,
|
||||
SIZEOF_REPEAT_QUESTFLAG_NUMBER = 8,
|
||||
FATIGUE_RESET_TIME = 0,
|
||||
|
||||
PC_FATIGUE_KILL_UNIT = 7,
|
||||
PC_FATIGUE_1_LEVEL = 11420,
|
||||
PC_FATIGUE_2_LEVEL = 6480,
|
||||
PC_FATIGUE_MAX_LEVEL = 2,
|
||||
PC_FUSIONMATTER_MAX = 999999999,
|
||||
PC_CANDY_MAX = 999999999,
|
||||
PC_BATTERY_MAX = 9999,
|
||||
PC_LEVEL_MAX = 36,
|
||||
SIZEOF_PC_BULLET_SLOT = 3,
|
||||
PC_TICK_TIME = 5000,
|
||||
SIZEOF_EQUIP_SLOT = 9,
|
||||
|
||||
EQUIP_SLOT_HAND = 0,
|
||||
EQUIP_SLOT_UPPERBODY = 1,
|
||||
EQUIP_SLOT_LOWERBODY = 2,
|
||||
EQUIP_SLOT_FOOT = 3,
|
||||
EQUIP_SLOT_HEAD = 4,
|
||||
EQUIP_SLOT_FACE = 5,
|
||||
EQUIP_SLOT_BACK = 6,
|
||||
EQUIP_SLOT_END = 6,
|
||||
EQUIP_SLOT_HAND_EX = 7,
|
||||
EQUIP_SLOT_VEHICLE = 8,
|
||||
|
||||
WPN_EQUIP_TYPE_NONE = 0,
|
||||
WPN_EQUIP_TYPE_OH_BLADE = 1,
|
||||
WPN_EQUIP_TYPE_OH_CLUB = 2,
|
||||
WPN_EQUIP_TYPE_OH_PISTOL = 3,
|
||||
WPN_EQUIP_TYPE_OH_RIPLE = 4,
|
||||
WPN_EQUIP_TYPE_OH_THROW = 5,
|
||||
WPN_EQUIP_TYPE_DH_BLADE = 6,
|
||||
WPN_EQUIP_TYPE_DH_CLUB = 7,
|
||||
WPN_EQUIP_TYPE_DH_DPISTOL = 8,
|
||||
WPN_EQUIP_TYPE_DH_RIPLE = 9,
|
||||
WPN_EQUIP_TYPE_DH_THROW = 10,
|
||||
WPN_EQUIP_TYPE_DH_ROCKET = 11,
|
||||
|
||||
SIZEOF_INVEN_SLOT = 50,
|
||||
SIZEOF_QINVEN_SLOT = 50,
|
||||
SIZEOF_BANK_SLOT = 119,
|
||||
SIZEOF_RESTORE_SLOT = 5,
|
||||
SIZEOF_NANO_BANK_SLOT = 37,
|
||||
SIZEOF_QUEST_SLOT = 1024,
|
||||
NANO_QUEST_INDEX = 0,
|
||||
SIZEOF_RQUEST_SLOT = 9,
|
||||
SIZEOF_QUESTFLAG_NUMBER = 32,
|
||||
SIZEOF_EP_RECORD_SLOT = 51,
|
||||
SIZEOF_TRADE_SLOT = 12,
|
||||
SIZEOF_VENDOR_TABLE_SLOT = 20,
|
||||
SIZEOF_VENDOR_RESTORE_SLOT = 5,
|
||||
SIZEOF_QUEST_NPC_SLOT = 3,
|
||||
SIZEOF_QUEST_ITEM_SLOT = 3,
|
||||
SIZEOF_MAX_ITEM_STACK = 100,
|
||||
SIZEOF_PC_SKILL_SLOT = 33,
|
||||
SIZEOF_QUICK_SLOT = 8,
|
||||
ENCHANT_WEAPON_MATERIAL_ID = 101,
|
||||
ENCHANT_DEFENCE_MATERIAL_ID = 102,
|
||||
SIZEOF_NANO_CARRY_SLOT = 3,
|
||||
COUNTOF_NANO_PER_SET = 3,
|
||||
SIZEOF_NANO_SET = 13,
|
||||
SIZEOF_NANO_STYLE = 3,
|
||||
NANO_STYLE_NONE = 1,
|
||||
NANO_STYLE_CRYSTAL = 0,
|
||||
NANO_STYLE_ENERGY = 1,
|
||||
NANO_STYLE_FLUID = 2,
|
||||
SIZEOF_NANO_TYPE = 4,
|
||||
NANO_TYPE_POWER = 0,
|
||||
NANO_TYPE_ACCURACY = 1,
|
||||
NANO_TYPE_PROTECT = 2,
|
||||
NANO_TYPE_DODGE = 3,
|
||||
SIZEOF_NANO_TUNE_NEED_ITEM_SLOT = 10,
|
||||
VALUE_ATTACK_MISS = 1,
|
||||
|
||||
MSG_ONLINE = 1,
|
||||
MSG_BUSY = 2,
|
||||
MSG_OFFLINE = 0,
|
||||
SIZEOF_FREE_CHAT_STRING = 128,
|
||||
SIZEOF_MENU_CHAT_STRING = 128,
|
||||
SIZEOF_BUDDYLIST_SLOT = 50,
|
||||
SIZEOF_EMAIL_SUBJECT_STRING = 32,
|
||||
SIZEOF_EMAIL_CONTENT_STRING = 512,
|
||||
SIZEOF_EMAIL_PAGE_SIZE = 5,
|
||||
SIZEOF_EMAIL_ITEM_CNT = 4,
|
||||
EMAIL_AND_MONEY_COST = 50,
|
||||
EMAIL_ITEM_COST = 20,
|
||||
BUDDYWARP_INTERVAL = 60,
|
||||
EMAILSEND_TIME_DELAY = 60,
|
||||
DB_ERROR_INVALID_DATA = 1,
|
||||
DB_ERROR_HACK_ATTEMPT = 2,
|
||||
DB_ERROR_ACCESS_FAIL = 3,
|
||||
DB_ERROR_PC_INSERT_FAIL = 4,
|
||||
CALL_NPC_MAX_CNT = 2048,
|
||||
|
||||
CN_EP_RING_MAX_CNT = 999,
|
||||
|
||||
HF_BIT_NONE = 0,
|
||||
HF_BIT_NORMAL = 1,
|
||||
HF_BIT_CRITICAL = 2,
|
||||
HF_BIT_STYLE_WIN = 4,
|
||||
HF_BIT_STYLE_TIE = 8,
|
||||
HF_BIT_STYLE_LOSE = 16,
|
||||
SKIN_COLOR_MAX = 12,
|
||||
HAIR_COLOR_MAX = 18,
|
||||
EYE_COLOR_MAX = 5,
|
||||
BODY_TYPE_MAX = 3,
|
||||
HEIGHT_TYPE_MAX = 5,
|
||||
CLASS_TYPE_MAX = 4,
|
||||
CN_EP_RACE_MODE_PRACTICE = 0,
|
||||
CN_EP_RACE_MODE_RECORD = 1,
|
||||
CN_EP_SECOM_NPC_TYPE_NUM = 13,
|
||||
CN_EP_EECOM_NPC_TYPE_NUM = 14,
|
||||
CN_EP_SIZE_SMALL = 0,
|
||||
CN_EP_SIZE_MIDDLE = 1,
|
||||
CN_EP_SIZE_BIG = 2,
|
||||
CN_EP_TICKET_ITEM_ID_SMALL = 115,
|
||||
CN_EP_TICKET_ITEM_ID_MIDDLE = 116,
|
||||
CN_EP_TICKET_ITEM_ID_BIG = 117,
|
||||
CN_EP_TICKET_ITEM_ID_FREE = 118,
|
||||
CN_EP_DISTANCE_ERROR_SAFE_RANGE = 1200,
|
||||
|
||||
CN_ACCOUNT_LEVEL__MASTER = 1,
|
||||
CN_ACCOUNT_LEVEL__POWER_DEVELOPER = 10,
|
||||
CN_ACCOUNT_LEVEL__QA = 20,
|
||||
CN_ACCOUNT_LEVEL__GM = 30,
|
||||
CN_ACCOUNT_LEVEL__CS = 40,
|
||||
CN_ACCOUNT_LEVEL__FREE_USER = 48,
|
||||
CN_ACCOUNT_LEVEL__PAY_USER = 49,
|
||||
CN_ACCOUNT_LEVEL__DEVELOPER = 50,
|
||||
CN_ACCOUNT_LEVEL__CLOSEBETA_USER = 80,
|
||||
CN_ACCOUNT_LEVEL__OPENBETA_USER = 85,
|
||||
CN_ACCOUNT_LEVEL__USER = 99,
|
||||
|
||||
CN_SPECIAL_STATE_FLAG__PRINT_GM = 1,
|
||||
CN_SPECIAL_STATE_FLAG__INVISIBLE = 2,
|
||||
CN_SPECIAL_STATE_FLAG__INVULNERABLE = 4,
|
||||
CN_SPECIAL_STATE_FLAG__FULL_UI = 16,
|
||||
CN_SPECIAL_STATE_FLAG__COMBAT = 32,
|
||||
CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT = 64,
|
||||
|
||||
CN_GM_SET_VALUE_TYPE__HP = 1,
|
||||
CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY = 2,
|
||||
CN_GM_SET_VALUE_TYPE__NANO_BATTERY = 3,
|
||||
CN_GM_SET_VALUE_TYPE__FUSION_MATTER = 4,
|
||||
CN_GM_SET_VALUE_TYPE__CANDY = 5,
|
||||
CN_GM_SET_VALUE_TYPE__SPEED = 6,
|
||||
CN_GM_SET_VALUE_TYPE__JUMP = 7,
|
||||
CN_GM_SET_VALUE_TYPE__END = 8,
|
||||
|
||||
HEIGHT_CLIMBABLE = 150,
|
||||
CN_GROUP_WARP_CHECK_RANGE = 1000,
|
||||
WYVERN_LOCATION_FLAG_SIZE = 2,
|
||||
CN_PC_EVENT_ID_GET_NANO_QUEST = 1,
|
||||
CN_PC_EVENT_ID_DEFEAT_FUSE_AND_GET_NANO = 2,
|
||||
_dCN_STREETSTALL__ITEMLIST_COUNT_MAX = 5,
|
||||
|
||||
CSB_BIT_NONE = 0, // 0
|
||||
CSB_BIT_UP_MOVE_SPEED = 0x1, // 1
|
||||
CSB_BIT_UP_SWIM_SPEED = 0x2, // 2
|
||||
CSB_BIT_UP_JUMP_HEIGHT = 0x4, // 4
|
||||
CSB_BIT_UP_STEALTH = 0x8, // 8
|
||||
CSB_BIT_PHOENIX = 0x10, // 16
|
||||
CSB_BIT_PROTECT_BATTERY = 0x20, // 32
|
||||
CSB_BIT_PROTECT_INFECTION = 0x40, // 64
|
||||
CSB_BIT_DN_MOVE_SPEED = 0x80, // 128
|
||||
CSB_BIT_DN_ATTACK_SPEED = 0x100, // 256
|
||||
CSB_BIT_STUN = 0x200, // 512
|
||||
CSB_BIT_MEZ = 0x400, // 1024
|
||||
CSB_BIT_KNOCKDOWN = 0x800, // 2048
|
||||
CSB_BIT_MINIMAP_ENEMY = 0x1000, // 4096
|
||||
CSB_BIT_MINIMAP_TRESURE = 0x2000, // 8192
|
||||
CSB_BIT_REWARD_BLOB = 0x4000, // 16384
|
||||
CSB_BIT_REWARD_CASH = 0x8000, // 32768
|
||||
CSB_BIT_INFECTION = 0x10000, // 65536
|
||||
CSB_BIT_FREEDOM = 0x20000, // 131072
|
||||
CSB_BIT_BOUNDINGBALL = 0x40000, // 262144
|
||||
CSB_BIT_INVULNERABLE = 0x80000, // 524288
|
||||
CSB_BIT_STIMPAKSLOT1 = 0x100000, // 1048576
|
||||
CSB_BIT_STIMPAKSLOT2 = 0x200000, // 2097152
|
||||
CSB_BIT_STIMPAKSLOT3 = 0x400000, // 4194304
|
||||
CSB_BIT_HEAL = 0x800000, // 8388608
|
||||
CSB_BIT_EXTRABANK = 0x1000000, // 16777216
|
||||
|
||||
TIME_BUFF_CONFIRM_KEY_MAX = 2000000000,
|
||||
READPACKET_SUCC = 0,
|
||||
READPACKET_FAIL = 1,
|
||||
READPACKET_RETURN = 2,
|
||||
BITMASK_FROM2TO = 0x7fffffff, // 2147483647
|
||||
BITMASK_FROM = 0x7fffffff, // 2147483647
|
||||
BITMASK_TO = 0xf000000, // 251658240
|
||||
BITMASK_SENDBLOCK = 0x800000, // 8388608
|
||||
BITMASK_AUTHED = 0x400000, // 4194304
|
||||
BITMASK_U_ID = 0xfff, // 4095
|
||||
|
||||
CL2LS = 0x12000000,
|
||||
CL2FE = 0x13000000,
|
||||
LS2CL = 0x21000000,
|
||||
LS2LS = 0x22000000,
|
||||
LS2DBA = 0x27000000,
|
||||
FE2CL = 0x31000000,
|
||||
FE2FE = 0x33000000,
|
||||
FE2GS = 0x34000000,
|
||||
FE2EP = 0x36000000,
|
||||
FE2MSG = 0x38000000,
|
||||
GS2FE = 0x43000000,
|
||||
GS2GS = 0x44000000,
|
||||
GS2AI = 0x45000000,
|
||||
GS2EP = 0x46000000,
|
||||
GS2DBA = 0x47000000,
|
||||
GS2MSG = 0x48000000,
|
||||
GS2MGR = 0x4a000000,
|
||||
AI2GS = 0x54000000,
|
||||
EP2FE = 0x63000000,
|
||||
EP2GS = 0x64000000,
|
||||
DBA2GS = 0x74000000,
|
||||
DBA2EP = 0x75000000,
|
||||
MSG2FE = 0x7fffffff,
|
||||
MSG2GS = 0x7fffffff,
|
||||
MSG2CMSG = 0x7fffffff,
|
||||
CMSG2MSG = 0x7fffffff,
|
||||
MGR2SPY = 0x7fffffff,
|
||||
SPY2MGR = 0x7fffffff,
|
||||
MGR2SA = 0x7fffffff,
|
||||
SA2MGR = 0x7fffffff,
|
||||
SA2SPY = 0x7fffffff,
|
||||
SPY2SA = 0x7fffffff,
|
||||
SPY2SVR = 0x7fffffff,
|
||||
SVR2SPY = 0x7fffffff,
|
||||
SCH2SVR = 0x7fffffff,
|
||||
SCH2LS = 0x7fffffff,
|
||||
SCH2FE = 0x7fffffff,
|
||||
SCH2GS = 0x7fffffff,
|
||||
SCH2AI = 0x7fffffff,
|
||||
SCH2EP = 0x7fffffff,
|
||||
SCH2DBA = 0x7fffffff,
|
||||
SCH2MSG = 0x7fffffff,
|
||||
SCH2CMSG = 0x7fffffff,
|
||||
CL2CDR = 0x1f000000,
|
||||
|
||||
SENDBLOCK = 0x800000, // 8388608
|
||||
AUTHED_X = 0, // 0
|
||||
AUTHED_O = 0x400000, // 4194304
|
||||
|
||||
SEND_SVR_FE = 1,
|
||||
SEND_SVR_FE_ANY = 2,
|
||||
SEND_SVR_FE_ALL = 3,
|
||||
SEND_SVR_AI = 4,
|
||||
SEND_SVR_AI_ANY = 5,
|
||||
SEND_SVR_AI_ALL = 6,
|
||||
SEND_SVR_FE_AI_ALL = 7,
|
||||
SEND_SVR_DBA = 8,
|
||||
SEND_SVR_GS = 9,
|
||||
SEND_SVR_MSG = 10,
|
||||
SEND_SVR_MSG_ANY = 11,
|
||||
SEND_SVR_MSG_ALL = 12,
|
||||
|
||||
SEND_UNICAST = 1,
|
||||
SEND_ANYCAST = 2,
|
||||
SEND_ANYCAST_NEW = 3,
|
||||
SEND_BROADCAST = 4,
|
||||
|
||||
CN_PACKET_BUFFER_SIZE = 4096,
|
||||
|
||||
P_CL2LS_REQ_LOGIN = 0x12000001, // 301989889
|
||||
P_CL2LS_REQ_CHECK_CHAR_NAME = 0x12000002, // 301989890
|
||||
P_CL2LS_REQ_SAVE_CHAR_NAME = 0x12000003, // 301989891
|
||||
P_CL2LS_REQ_CHAR_CREATE = 0x12000004, // 301989892
|
||||
P_CL2LS_REQ_CHAR_SELECT = 0x12000005, // 301989893
|
||||
P_CL2LS_REQ_CHAR_DELETE = 0x12000006, // 301989894
|
||||
P_CL2LS_REQ_SHARD_SELECT = 0x12000007, // 301989895
|
||||
P_CL2LS_REQ_SHARD_LIST_INFO = 0x12000008, // 301989896
|
||||
P_CL2LS_CHECK_NAME_LIST = 0x12000009, // 301989897
|
||||
P_CL2LS_REQ_SAVE_CHAR_TUTOR = 0x1200000a, // 301989898
|
||||
P_CL2LS_REQ_PC_EXIT_DUPLICATE = 0x1200000b, // 301989899
|
||||
P_CL2LS_REP_LIVE_CHECK = 0x1200000c, // 301989900
|
||||
P_CL2LS_REQ_CHANGE_CHAR_NAME = 0x1200000d, // 301989901
|
||||
P_CL2LS_REQ_SERVER_SELECT = 0x1200000e, // 301989902
|
||||
|
||||
P_CL2FE_REQ_PC_ENTER = 0x13000001, // 318767105
|
||||
P_CL2FE_REQ_PC_EXIT = 0x13000002, // 318767106
|
||||
P_CL2FE_REQ_PC_MOVE = 0x13000003, // 318767107
|
||||
P_CL2FE_REQ_PC_STOP = 0x13000004, // 318767108
|
||||
P_CL2FE_REQ_PC_JUMP = 0x13000005, // 318767109
|
||||
P_CL2FE_REQ_PC_ATTACK_NPCs = 0x13000006, // 318767110
|
||||
P_CL2FE_REQ_SEND_FREECHAT_MESSAGE = 0x13000007, // 318767111
|
||||
P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE = 0x13000008, // 318767112
|
||||
P_CL2FE_REQ_PC_REGEN = 0x13000009, // 318767113
|
||||
P_CL2FE_REQ_ITEM_MOVE = 0x1300000a, // 318767114
|
||||
P_CL2FE_REQ_PC_TASK_START = 0x1300000b, // 318767115
|
||||
P_CL2FE_REQ_PC_TASK_END = 0x1300000c, // 318767116
|
||||
P_CL2FE_REQ_NANO_EQUIP = 0x1300000d, // 318767117
|
||||
P_CL2FE_REQ_NANO_UNEQUIP = 0x1300000e, // 318767118
|
||||
P_CL2FE_REQ_NANO_ACTIVE = 0x1300000f, // 318767119
|
||||
P_CL2FE_REQ_NANO_TUNE = 0x13000010, // 318767120
|
||||
P_CL2FE_REQ_NANO_SKILL_USE = 0x13000011, // 318767121
|
||||
P_CL2FE_REQ_PC_TASK_STOP = 0x13000012, // 318767122
|
||||
P_CL2FE_REQ_PC_TASK_CONTINUE = 0x13000013, // 318767123
|
||||
P_CL2FE_REQ_PC_GOTO = 0x13000014, // 318767124
|
||||
P_CL2FE_REQ_CHARGE_NANO_STAMINA = 0x13000015, // 318767125
|
||||
P_CL2FE_REQ_PC_KILL_QUEST_NPCs = 0x13000016, // 318767126
|
||||
P_CL2FE_REQ_PC_VENDOR_ITEM_BUY = 0x13000017, // 318767127
|
||||
P_CL2FE_REQ_PC_VENDOR_ITEM_SELL = 0x13000018, // 318767128
|
||||
P_CL2FE_REQ_PC_ITEM_DELETE = 0x13000019, // 318767129
|
||||
P_CL2FE_REQ_PC_GIVE_ITEM = 0x1300001a, // 318767130
|
||||
P_CL2FE_REQ_PC_ROCKET_STYLE_READY = 0x1300001b, // 318767131
|
||||
P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE = 0x1300001c, // 318767132
|
||||
P_CL2FE_REQ_PC_ROCKET_STYLE_HIT = 0x1300001d, // 318767133
|
||||
P_CL2FE_REQ_PC_GRENADE_STYLE_READY = 0x1300001e, // 318767134
|
||||
P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE = 0x1300001f, // 318767135
|
||||
P_CL2FE_REQ_PC_GRENADE_STYLE_HIT = 0x13000020, // 318767136
|
||||
P_CL2FE_REQ_PC_NANO_CREATE = 0x13000021, // 318767137
|
||||
P_CL2FE_REQ_PC_TRADE_OFFER = 0x13000022, // 318767138
|
||||
P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL = 0x13000023, // 318767139
|
||||
P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT = 0x13000024, // 318767140
|
||||
P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL = 0x13000025, // 318767141
|
||||
P_CL2FE_REQ_PC_TRADE_OFFER_ABORT = 0x13000026, // 318767142
|
||||
P_CL2FE_REQ_PC_TRADE_CONFIRM = 0x13000027, // 318767143
|
||||
P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL = 0x13000028, // 318767144
|
||||
P_CL2FE_REQ_PC_TRADE_CONFIRM_ABORT = 0x13000029, // 318767145
|
||||
P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER = 0x1300002a, // 318767146
|
||||
P_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER = 0x1300002b, // 318767147
|
||||
P_CL2FE_REQ_PC_TRADE_CASH_REGISTER = 0x1300002c, // 318767148
|
||||
P_CL2FE_REQ_PC_TRADE_EMOTES_CHAT = 0x1300002d, // 318767149
|
||||
P_CL2FE_REQ_PC_BANK_OPEN = 0x1300002e, // 318767150
|
||||
P_CL2FE_REQ_PC_BANK_CLOSE = 0x1300002f, // 318767151
|
||||
P_CL2FE_REQ_PC_VENDOR_START = 0x13000030, // 318767152
|
||||
P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE = 0x13000031, // 318767153
|
||||
P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY = 0x13000032, // 318767154
|
||||
P_CL2FE_REQ_PC_COMBAT_BEGIN = 0x13000033, // 318767155
|
||||
P_CL2FE_REQ_PC_COMBAT_END = 0x13000034, // 318767156
|
||||
P_CL2FE_REQ_REQUEST_MAKE_BUDDY = 0x13000035, // 318767157
|
||||
P_CL2FE_REQ_ACCEPT_MAKE_BUDDY = 0x13000036, // 318767158
|
||||
P_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE = 0x13000037, // 318767159
|
||||
P_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE = 0x13000038, // 318767160
|
||||
P_CL2FE_REQ_GET_BUDDY_STYLE = 0x13000039, // 318767161
|
||||
P_CL2FE_REQ_SET_BUDDY_BLOCK = 0x1300003a, // 318767162
|
||||
P_CL2FE_REQ_REMOVE_BUDDY = 0x1300003b, // 318767163
|
||||
P_CL2FE_REQ_GET_BUDDY_STATE = 0x1300003c, // 318767164
|
||||
P_CL2FE_REQ_PC_JUMPPAD = 0x1300003d, // 318767165
|
||||
P_CL2FE_REQ_PC_LAUNCHER = 0x1300003e, // 318767166
|
||||
P_CL2FE_REQ_PC_ZIPLINE = 0x1300003f, // 318767167
|
||||
P_CL2FE_REQ_PC_MOVEPLATFORM = 0x13000040, // 318767168
|
||||
P_CL2FE_REQ_PC_SLOPE = 0x13000041, // 318767169
|
||||
P_CL2FE_REQ_PC_STATE_CHANGE = 0x13000042, // 318767170
|
||||
P_CL2FE_REQ_PC_MAP_WARP = 0x13000043, // 318767171
|
||||
P_CL2FE_REQ_PC_GIVE_NANO = 0x13000044, // 318767172
|
||||
P_CL2FE_REQ_NPC_SUMMON = 0x13000045, // 318767173
|
||||
P_CL2FE_REQ_NPC_UNSUMMON = 0x13000046, // 318767174
|
||||
P_CL2FE_REQ_ITEM_CHEST_OPEN = 0x13000047, // 318767175
|
||||
P_CL2FE_REQ_PC_GIVE_NANO_SKILL = 0x13000048, // 318767176
|
||||
P_CL2FE_DOT_DAMAGE_ONOFF = 0x13000049, // 318767177
|
||||
P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY = 0x1300004a, // 318767178
|
||||
P_CL2FE_REQ_PC_WARP_USE_NPC = 0x1300004b, // 318767179
|
||||
P_CL2FE_REQ_PC_GROUP_INVITE = 0x1300004c, // 318767180
|
||||
P_CL2FE_REQ_PC_GROUP_INVITE_REFUSE = 0x1300004d, // 318767181
|
||||
P_CL2FE_REQ_PC_GROUP_JOIN = 0x1300004e, // 318767182
|
||||
P_CL2FE_REQ_PC_GROUP_LEAVE = 0x1300004f, // 318767183
|
||||
P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT = 0x13000050, // 318767184
|
||||
P_CL2FE_REQ_PC_BUDDY_WARP = 0x13000051, // 318767185
|
||||
P_CL2FE_REQ_GET_MEMBER_STYLE = 0x13000052, // 318767186
|
||||
P_CL2FE_REQ_GET_GROUP_STYLE = 0x13000053, // 318767187
|
||||
P_CL2FE_REQ_PC_CHANGE_MENTOR = 0x13000054, // 318767188
|
||||
P_CL2FE_REQ_GET_BUDDY_LOCATION = 0x13000055, // 318767189
|
||||
P_CL2FE_REQ_NPC_GROUP_SUMMON = 0x13000056, // 318767190
|
||||
P_CL2FE_REQ_PC_WARP_TO_PC = 0x13000057, // 318767191
|
||||
P_CL2FE_REQ_EP_RANK_GET_LIST = 0x13000058, // 318767192
|
||||
P_CL2FE_REQ_EP_RANK_GET_DETAIL = 0x13000059, // 318767193
|
||||
P_CL2FE_REQ_EP_RANK_GET_PC_INFO = 0x1300005a, // 318767194
|
||||
P_CL2FE_REQ_EP_RACE_START = 0x1300005b, // 318767195
|
||||
P_CL2FE_REQ_EP_RACE_END = 0x1300005c, // 318767196
|
||||
P_CL2FE_REQ_EP_RACE_CANCEL = 0x1300005d, // 318767197
|
||||
P_CL2FE_REQ_EP_GET_RING = 0x1300005e, // 318767198
|
||||
P_CL2FE_REQ_IM_CHANGE_SWITCH_STATUS = 0x1300005f, // 318767199
|
||||
P_CL2FE_REQ_SHINY_PICKUP = 0x13000060, // 318767200
|
||||
P_CL2FE_REQ_SHINY_SUMMON = 0x13000061, // 318767201
|
||||
P_CL2FE_REQ_PC_MOVETRANSPORTATION = 0x13000062, // 318767202
|
||||
P_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE = 0x13000063, // 318767203
|
||||
P_CL2FE_REQ_SEND_ANY_GROUP_FREECHAT_MESSAGE = 0x13000064, // 318767204
|
||||
P_CL2FE_REQ_BARKER = 0x13000065, // 318767205
|
||||
P_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE = 0x13000066, // 318767206
|
||||
P_CL2FE_REQ_SEND_ANY_GROUP_MENUCHAT_MESSAGE = 0x13000067, // 318767207
|
||||
P_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION = 0x13000068, // 318767208
|
||||
P_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION = 0x13000069, // 318767209
|
||||
P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH = 0x1300006a, // 318767210
|
||||
P_CL2FE_GM_REQ_PC_SET_VALUE = 0x1300006b, // 318767211
|
||||
P_CL2FE_GM_REQ_KICK_PLAYER = 0x1300006c, // 318767212
|
||||
P_CL2FE_GM_REQ_TARGET_PC_TELEPORT = 0x1300006d, // 318767213
|
||||
P_CL2FE_GM_REQ_PC_LOCATION = 0x1300006e, // 318767214
|
||||
P_CL2FE_GM_REQ_PC_ANNOUNCE = 0x1300006f, // 318767215
|
||||
P_CL2FE_REQ_SET_PC_BLOCK = 0x13000070, // 318767216
|
||||
P_CL2FE_REQ_REGIST_RXCOM = 0x13000071, // 318767217
|
||||
P_CL2FE_GM_REQ_PC_MOTD_REGISTER = 0x13000072, // 318767218
|
||||
P_CL2FE_REQ_ITEM_USE = 0x13000073, // 318767219
|
||||
P_CL2FE_REQ_WARP_USE_RECALL = 0x13000074, // 318767220
|
||||
P_CL2FE_REP_LIVE_CHECK = 0x13000075, // 318767221
|
||||
P_CL2FE_REQ_PC_MISSION_COMPLETE = 0x13000076, // 318767222
|
||||
P_CL2FE_REQ_PC_TASK_COMPLETE = 0x13000077, // 318767223
|
||||
P_CL2FE_REQ_NPC_INTERACTION = 0x13000078, // 318767224
|
||||
P_CL2FE_DOT_HEAL_ONOFF = 0x13000079, // 318767225
|
||||
P_CL2FE_REQ_PC_SPECIAL_STATE_SWITCH = 0x1300007a, // 318767226
|
||||
P_CL2FE_REQ_PC_EMAIL_UPDATE_CHECK = 0x1300007b, // 318767227
|
||||
P_CL2FE_REQ_PC_READ_EMAIL = 0x1300007c, // 318767228
|
||||
P_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST = 0x1300007d, // 318767229
|
||||
P_CL2FE_REQ_PC_DELETE_EMAIL = 0x1300007e, // 318767230
|
||||
P_CL2FE_REQ_PC_SEND_EMAIL = 0x1300007f, // 318767231
|
||||
P_CL2FE_REQ_PC_RECV_EMAIL_ITEM = 0x13000080, // 318767232
|
||||
P_CL2FE_REQ_PC_RECV_EMAIL_CANDY = 0x13000081, // 318767233
|
||||
P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF = 0x13000082, // 318767234
|
||||
P_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID = 0x13000083, // 318767235
|
||||
P_CL2FE_REQ_NPC_GROUP_INVITE = 0x13000084, // 318767236
|
||||
P_CL2FE_REQ_NPC_GROUP_KICK = 0x13000085, // 318767237
|
||||
P_CL2FE_REQ_PC_FIRST_USE_FLAG_SET = 0x13000086, // 318767238
|
||||
P_CL2FE_REQ_PC_TRANSPORT_WARP = 0x13000087, // 318767239
|
||||
P_CL2FE_REQ_PC_TIME_TO_GO_WARP = 0x13000088, // 318767240
|
||||
P_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL = 0x13000089, // 318767241
|
||||
P_CL2FE_REQ_CHANNEL_INFO = 0x1300008a, // 318767242
|
||||
P_CL2FE_REQ_PC_CHANNEL_NUM = 0x1300008b, // 318767243
|
||||
P_CL2FE_REQ_PC_WARP_CHANNEL = 0x1300008c, // 318767244
|
||||
P_CL2FE_REQ_PC_LOADING_COMPLETE = 0x1300008d, // 318767245
|
||||
P_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY = 0x1300008e, // 318767246
|
||||
P_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY = 0x1300008f, // 318767247
|
||||
P_CL2FE_REQ_PC_ATTACK_CHARs = 0x13000090, // 318767248
|
||||
P_CL2FE_PC_STREETSTALL_REQ_READY = 0x13000091, // 318767249
|
||||
P_CL2FE_PC_STREETSTALL_REQ_CANCEL = 0x13000092, // 318767250
|
||||
P_CL2FE_PC_STREETSTALL_REQ_REGIST_ITEM = 0x13000093, // 318767251
|
||||
P_CL2FE_PC_STREETSTALL_REQ_UNREGIST_ITEM = 0x13000094, // 318767252
|
||||
P_CL2FE_PC_STREETSTALL_REQ_SALE_START = 0x13000095, // 318767253
|
||||
P_CL2FE_PC_STREETSTALL_REQ_ITEM_LIST = 0x13000096, // 318767254
|
||||
P_CL2FE_PC_STREETSTALL_REQ_ITEM_BUY = 0x13000097, // 318767255
|
||||
P_CL2FE_REQ_PC_ITEM_COMBINATION = 0x13000098, // 318767256
|
||||
P_CL2FE_GM_REQ_SET_PC_SKILL = 0x13000099, // 318767257
|
||||
P_CL2FE_REQ_PC_SKILL_ADD = 0x1300009a, // 318767258
|
||||
P_CL2FE_REQ_PC_SKILL_DEL = 0x1300009b, // 318767259
|
||||
P_CL2FE_REQ_PC_SKILL_USE = 0x1300009c, // 318767260
|
||||
P_CL2FE_REQ_PC_ROPE = 0x1300009d, // 318767261
|
||||
P_CL2FE_REQ_PC_BELT = 0x1300009e, // 318767262
|
||||
P_CL2FE_REQ_PC_VEHICLE_ON = 0x1300009f, // 318767263
|
||||
P_CL2FE_REQ_PC_VEHICLE_OFF = 0x130000a0, // 318767264
|
||||
P_CL2FE_REQ_PC_REGIST_QUICK_SLOT = 0x130000a1, // 318767265
|
||||
P_CL2FE_REQ_PC_DISASSEMBLE_ITEM = 0x130000a2, // 318767266
|
||||
P_CL2FE_GM_REQ_REWARD_RATE = 0x130000a3, // 318767267
|
||||
P_CL2FE_REQ_PC_ITEM_ENCHANT = 0x130000a4, // 318767268
|
||||
|
||||
P_FE2CL_ERROR = 0x31000000, // 822083584
|
||||
P_FE2CL_REP_PC_ENTER_FAIL = 0x31000001, // 822083585
|
||||
P_FE2CL_REP_PC_ENTER_SUCC = 0x31000002, // 822083586
|
||||
P_FE2CL_PC_NEW = 0x31000003, // 822083587
|
||||
P_FE2CL_REP_PC_EXIT_FAIL = 0x31000004, // 822083588
|
||||
P_FE2CL_REP_PC_EXIT_SUCC = 0x31000005, // 822083589
|
||||
P_FE2CL_PC_EXIT = 0x31000006, // 822083590
|
||||
P_FE2CL_PC_AROUND = 0x31000007, // 822083591
|
||||
P_FE2CL_PC_MOVE = 0x31000008, // 822083592
|
||||
P_FE2CL_PC_STOP = 0x31000009, // 822083593
|
||||
P_FE2CL_PC_JUMP = 0x3100000a, // 822083594
|
||||
P_FE2CL_NPC_ENTER = 0x3100000b, // 822083595
|
||||
P_FE2CL_NPC_EXIT = 0x3100000c, // 822083596
|
||||
P_FE2CL_NPC_MOVE = 0x3100000d, // 822083597
|
||||
P_FE2CL_NPC_NEW = 0x3100000e, // 822083598
|
||||
P_FE2CL_NPC_AROUND = 0x3100000f, // 822083599
|
||||
P_FE2CL_AROUND_DEL_PC = 0x31000010, // 822083600
|
||||
P_FE2CL_AROUND_DEL_NPC = 0x31000011, // 822083601
|
||||
P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC = 0x31000012, // 822083602
|
||||
P_FE2CL_REP_SEND_FREECHAT_MESSAGE_FAIL = 0x31000013, // 822083603
|
||||
P_FE2CL_PC_ATTACK_NPCs_SUCC = 0x31000014, // 822083604
|
||||
P_FE2CL_PC_ATTACK_NPCs = 0x31000015, // 822083605
|
||||
P_FE2CL_NPC_ATTACK_PCs = 0x31000016, // 822083606
|
||||
P_FE2CL_REP_PC_REGEN_SUCC = 0x31000017, // 822083607
|
||||
P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC = 0x31000018, // 822083608
|
||||
P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_FAIL = 0x31000019, // 822083609
|
||||
P_FE2CL_PC_ITEM_MOVE_SUCC = 0x3100001a, // 822083610
|
||||
P_FE2CL_PC_EQUIP_CHANGE = 0x3100001b, // 822083611
|
||||
P_FE2CL_REP_PC_TASK_START_SUCC = 0x3100001c, // 822083612
|
||||
P_FE2CL_REP_PC_TASK_START_FAIL = 0x3100001d, // 822083613
|
||||
P_FE2CL_REP_PC_TASK_END_SUCC = 0x3100001e, // 822083614
|
||||
P_FE2CL_REP_PC_TASK_END_FAIL = 0x3100001f, // 822083615
|
||||
P_FE2CL_NPC_SKILL_READY = 0x31000020, // 822083616
|
||||
P_FE2CL_NPC_SKILL_FIRE = 0x31000021, // 822083617
|
||||
P_FE2CL_NPC_SKILL_HIT = 0x31000022, // 822083618
|
||||
P_FE2CL_NPC_SKILL_CORRUPTION_READY = 0x31000023, // 822083619
|
||||
P_FE2CL_NPC_SKILL_CORRUPTION_HIT = 0x31000024, // 822083620
|
||||
P_FE2CL_NPC_SKILL_CANCEL = 0x31000025, // 822083621
|
||||
P_FE2CL_REP_NANO_EQUIP_SUCC = 0x31000026, // 822083622
|
||||
P_FE2CL_REP_NANO_UNEQUIP_SUCC = 0x31000027, // 822083623
|
||||
P_FE2CL_REP_NANO_ACTIVE_SUCC = 0x31000028, // 822083624
|
||||
P_FE2CL_REP_NANO_TUNE_SUCC = 0x31000029, // 822083625
|
||||
P_FE2CL_NANO_ACTIVE = 0x3100002a, // 822083626
|
||||
P_FE2CL_NANO_SKILL_USE_SUCC = 0x3100002b, // 822083627
|
||||
P_FE2CL_NANO_SKILL_USE = 0x3100002c, // 822083628
|
||||
P_FE2CL_REP_PC_TASK_STOP_SUCC = 0x3100002d, // 822083629
|
||||
P_FE2CL_REP_PC_TASK_STOP_FAIL = 0x3100002e, // 822083630
|
||||
P_FE2CL_REP_PC_TASK_CONTINUE_SUCC = 0x3100002f, // 822083631
|
||||
P_FE2CL_REP_PC_TASK_CONTINUE_FAIL = 0x31000030, // 822083632
|
||||
P_FE2CL_REP_PC_GOTO_SUCC = 0x31000031, // 822083633
|
||||
P_FE2CL_REP_CHARGE_NANO_STAMINA = 0x31000032, // 822083634
|
||||
P_FE2CL_REP_PC_TICK = 0x31000033, // 822083635
|
||||
P_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC = 0x31000034, // 822083636
|
||||
P_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC = 0x31000035, // 822083637
|
||||
P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL = 0x31000036, // 822083638
|
||||
P_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC = 0x31000037, // 822083639
|
||||
P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL = 0x31000038, // 822083640
|
||||
P_FE2CL_REP_PC_ITEM_DELETE_SUCC = 0x31000039, // 822083641
|
||||
P_FE2CL_PC_ROCKET_STYLE_READY = 0x3100003a, // 822083642
|
||||
P_FE2CL_REP_PC_ROCKET_STYLE_FIRE_SUCC = 0x3100003b, // 822083643
|
||||
P_FE2CL_PC_ROCKET_STYLE_FIRE = 0x3100003c, // 822083644
|
||||
P_FE2CL_PC_ROCKET_STYLE_HIT = 0x3100003d, // 822083645
|
||||
P_FE2CL_PC_GRENADE_STYLE_READY = 0x3100003e, // 822083646
|
||||
P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC = 0x3100003f, // 822083647
|
||||
P_FE2CL_PC_GRENADE_STYLE_FIRE = 0x31000040, // 822083648
|
||||
P_FE2CL_PC_GRENADE_STYLE_HIT = 0x31000041, // 822083649
|
||||
P_FE2CL_REP_PC_TRADE_OFFER = 0x31000042, // 822083650
|
||||
P_FE2CL_REP_PC_TRADE_OFFER_CANCEL = 0x31000043, // 822083651
|
||||
P_FE2CL_REP_PC_TRADE_OFFER_SUCC = 0x31000044, // 822083652
|
||||
P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL = 0x31000045, // 822083653
|
||||
P_FE2CL_REP_PC_TRADE_OFFER_ABORT = 0x31000046, // 822083654
|
||||
P_FE2CL_REP_PC_TRADE_CONFIRM = 0x31000047, // 822083655
|
||||
P_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL = 0x31000048, // 822083656
|
||||
P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT = 0x31000049, // 822083657
|
||||
P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC = 0x3100004a, // 822083658
|
||||
P_FE2CL_REP_PC_TRADE_CONFIRM_FAIL = 0x3100004b, // 822083659
|
||||
P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC = 0x3100004c, // 822083660
|
||||
P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_FAIL = 0x3100004d, // 822083661
|
||||
P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC = 0x3100004e, // 822083662
|
||||
P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_FAIL = 0x3100004f, // 822083663
|
||||
P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC = 0x31000050, // 822083664
|
||||
P_FE2CL_REP_PC_TRADE_CASH_REGISTER_FAIL = 0x31000051, // 822083665
|
||||
P_FE2CL_REP_PC_TRADE_EMOTES_CHAT = 0x31000052, // 822083666
|
||||
P_FE2CL_REP_PC_NANO_CREATE_SUCC = 0x31000053, // 822083667
|
||||
P_FE2CL_REP_PC_NANO_CREATE_FAIL = 0x31000054, // 822083668
|
||||
P_FE2CL_REP_NANO_TUNE_FAIL = 0x31000055, // 822083669
|
||||
P_FE2CL_REP_PC_BANK_OPEN_SUCC = 0x31000056, // 822083670
|
||||
P_FE2CL_REP_PC_BANK_OPEN_FAIL = 0x31000057, // 822083671
|
||||
P_FE2CL_REP_PC_BANK_CLOSE_SUCC = 0x31000058, // 822083672
|
||||
P_FE2CL_REP_PC_BANK_CLOSE_FAIL = 0x31000059, // 822083673
|
||||
P_FE2CL_REP_PC_VENDOR_START_SUCC = 0x3100005a, // 822083674
|
||||
P_FE2CL_REP_PC_VENDOR_START_FAIL = 0x3100005b, // 822083675
|
||||
P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC = 0x3100005c, // 822083676
|
||||
P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_FAIL = 0x3100005d, // 822083677
|
||||
P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC = 0x3100005e, // 822083678
|
||||
P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL = 0x3100005f, // 822083679
|
||||
P_FE2CL_CHAR_TIME_BUFF_TIME_OUT = 0x31000060, // 822083680
|
||||
P_FE2CL_REP_PC_GIVE_ITEM_SUCC = 0x31000061, // 822083681
|
||||
P_FE2CL_REP_PC_GIVE_ITEM_FAIL = 0x31000062, // 822083682
|
||||
P_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC = 0x31000063, // 822083683
|
||||
P_FE2CL_REP_PC_BUDDYLIST_INFO_FAIL = 0x31000064, // 822083684
|
||||
P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC = 0x7fffffff, // 2147483647
|
||||
P_FE2CL_REP_REQUEST_MAKE_BUDDY_FAIL = 0x31000066, // 822083686
|
||||
P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC = 0x31000067, // 822083687
|
||||
P_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL = 0x31000068, // 822083688
|
||||
P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC = 0x31000069, // 822083689
|
||||
P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_FAIL = 0x3100006a, // 822083690
|
||||
P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC = 0x3100006b, // 822083691
|
||||
P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_FAIL = 0x3100006c, // 822083692
|
||||
P_FE2CL_REP_GET_BUDDY_STYLE_SUCC = 0x3100006d, // 822083693
|
||||
P_FE2CL_REP_GET_BUDDY_STYLE_FAIL = 0x3100006e, // 822083694
|
||||
P_FE2CL_REP_GET_BUDDY_STATE_SUCC = 0x3100006f, // 822083695
|
||||
P_FE2CL_REP_GET_BUDDY_STATE_FAIL = 0x31000070, // 822083696
|
||||
P_FE2CL_REP_SET_BUDDY_BLOCK_SUCC = 0x31000071, // 822083697
|
||||
P_FE2CL_REP_SET_BUDDY_BLOCK_FAIL = 0x31000072, // 822083698
|
||||
P_FE2CL_REP_REMOVE_BUDDY_SUCC = 0x31000073, // 822083699
|
||||
P_FE2CL_REP_REMOVE_BUDDY_FAIL = 0x31000074, // 822083700
|
||||
P_FE2CL_PC_JUMPPAD = 0x31000075, // 822083701
|
||||
P_FE2CL_PC_LAUNCHER = 0x31000076, // 822083702
|
||||
P_FE2CL_PC_ZIPLINE = 0x31000077, // 822083703
|
||||
P_FE2CL_PC_MOVEPLATFORM = 0x31000078, // 822083704
|
||||
P_FE2CL_PC_SLOPE = 0x31000079, // 822083705
|
||||
P_FE2CL_PC_STATE_CHANGE = 0x3100007a, // 822083706
|
||||
P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC_TO_ACCEPTER = 0x3100007b, // 822083707
|
||||
P_FE2CL_REP_REWARD_ITEM = 0x3100007c, // 822083708
|
||||
P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC = 0x3100007d, // 822083709
|
||||
P_FE2CL_REP_ITEM_CHEST_OPEN_FAIL = 0x3100007e, // 822083710
|
||||
P_FE2CL_CHAR_TIME_BUFF_TIME_TICK = 0x3100007f, // 822083711
|
||||
P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC = 0x31000080, // 822083712
|
||||
P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL = 0x31000081, // 822083713
|
||||
P_FE2CL_NPC_ROCKET_STYLE_FIRE = 0x31000082, // 822083714
|
||||
P_FE2CL_NPC_GRENADE_STYLE_FIRE = 0x31000083, // 822083715
|
||||
P_FE2CL_NPC_BULLET_STYLE_HIT = 0x31000084, // 822083716
|
||||
P_FE2CL_CHARACTER_ATTACK_CHARACTERs = 0x31000085, // 822083717
|
||||
P_FE2CL_PC_GROUP_INVITE = 0x31000086, // 822083718
|
||||
P_FE2CL_PC_GROUP_INVITE_FAIL = 0x31000087, // 822083719
|
||||
P_FE2CL_PC_GROUP_INVITE_REFUSE = 0x31000088, // 822083720
|
||||
P_FE2CL_PC_GROUP_JOIN = 0x31000089, // 822083721
|
||||
P_FE2CL_PC_GROUP_JOIN_FAIL = 0x3100008a, // 822083722
|
||||
P_FE2CL_PC_GROUP_JOIN_SUCC = 0x3100008b, // 822083723
|
||||
P_FE2CL_PC_GROUP_LEAVE = 0x3100008c, // 822083724
|
||||
P_FE2CL_PC_GROUP_LEAVE_FAIL = 0x3100008d, // 822083725
|
||||
P_FE2CL_PC_GROUP_LEAVE_SUCC = 0x3100008e, // 822083726
|
||||
P_FE2CL_PC_GROUP_MEMBER_INFO = 0x3100008f, // 822083727
|
||||
P_FE2CL_REP_PC_WARP_USE_NPC_SUCC = 0x31000090, // 822083728
|
||||
P_FE2CL_REP_PC_WARP_USE_NPC_FAIL = 0x31000091, // 822083729
|
||||
P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT = 0x31000092, // 822083730
|
||||
P_FE2CL_REP_PC_CHANGE_MENTOR_SUCC = 0x31000093, // 822083731
|
||||
P_FE2CL_REP_PC_CHANGE_MENTOR_FAIL = 0x31000094, // 822083732
|
||||
P_FE2CL_REP_GET_MEMBER_STYLE_FAIL = 0x31000095, // 822083733
|
||||
P_FE2CL_REP_GET_MEMBER_STYLE_SUCC = 0x31000096, // 822083734
|
||||
P_FE2CL_REP_GET_GROUP_STYLE_FAIL = 0x31000097, // 822083735
|
||||
P_FE2CL_REP_GET_GROUP_STYLE_SUCC = 0x31000098, // 822083736
|
||||
P_FE2CL_PC_REGEN = 0x31000099, // 822083737
|
||||
P_FE2CL_INSTANCE_MAP_INFO = 0x3100009a, // 822083738
|
||||
P_FE2CL_TRANSPORTATION_ENTER = 0x3100009b, // 822083739
|
||||
P_FE2CL_TRANSPORTATION_EXIT = 0x3100009c, // 822083740
|
||||
P_FE2CL_TRANSPORTATION_MOVE = 0x3100009d, // 822083741
|
||||
P_FE2CL_TRANSPORTATION_NEW = 0x3100009e, // 822083742
|
||||
P_FE2CL_TRANSPORTATION_AROUND = 0x3100009f, // 822083743
|
||||
P_FE2CL_AROUND_DEL_TRANSPORTATION = 0x310000a0, // 822083744
|
||||
P_FE2CL_REP_EP_RANK_LIST = 0x310000a1, // 822083745
|
||||
P_FE2CL_REP_EP_RANK_DETAIL = 0x310000a2, // 822083746
|
||||
P_FE2CL_REP_EP_RANK_PC_INFO = 0x310000a3, // 822083747
|
||||
P_FE2CL_REP_EP_RACE_START_SUCC = 0x310000a4, // 822083748
|
||||
P_FE2CL_REP_EP_RACE_START_FAIL = 0x310000a5, // 822083749
|
||||
P_FE2CL_REP_EP_RACE_END_SUCC = 0x310000a6, // 822083750
|
||||
P_FE2CL_REP_EP_RACE_END_FAIL = 0x310000a7, // 822083751
|
||||
P_FE2CL_REP_EP_RACE_CANCEL_SUCC = 0x310000a8, // 822083752
|
||||
P_FE2CL_REP_EP_RACE_CANCEL_FAIL = 0x310000a9, // 822083753
|
||||
P_FE2CL_REP_EP_GET_RING_SUCC = 0x310000aa, // 822083754
|
||||
P_FE2CL_REP_EP_GET_RING_FAIL = 0x310000ab, // 822083755
|
||||
P_FE2CL_REP_IM_CHANGE_SWITCH_STATUS = 0x310000ac, // 822083756
|
||||
P_FE2CL_SHINY_ENTER = 0x310000ad, // 822083757
|
||||
P_FE2CL_SHINY_EXIT = 0x310000ae, // 822083758
|
||||
P_FE2CL_SHINY_NEW = 0x310000af, // 822083759
|
||||
P_FE2CL_SHINY_AROUND = 0x310000b0, // 822083760
|
||||
P_FE2CL_AROUND_DEL_SHINY = 0x310000b1, // 822083761
|
||||
P_FE2CL_REP_SHINY_PICKUP_FAIL = 0x310000b2, // 822083762
|
||||
P_FE2CL_REP_SHINY_PICKUP_SUCC = 0x310000b3, // 822083763
|
||||
P_FE2CL_PC_MOVETRANSPORTATION = 0x310000b4, // 822083764
|
||||
P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC = 0x310000b5, // 822083765
|
||||
P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_FAIL = 0x310000b6, // 822083766
|
||||
P_FE2CL_REP_SEND_ANY_GROUP_FREECHAT_MESSAGE_SUCC = 0x310000b7, // 822083767
|
||||
P_FE2CL_REP_SEND_ANY_GROUP_FREECHAT_MESSAGE_FAIL = 0x310000b8, // 822083768
|
||||
P_FE2CL_REP_BARKER = 0x310000b9, // 822083769
|
||||
P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC = 0x310000ba, // 822083770
|
||||
P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_FAIL = 0x310000bb, // 822083771
|
||||
P_FE2CL_REP_SEND_ANY_GROUP_MENUCHAT_MESSAGE_SUCC = 0x310000bc, // 822083772
|
||||
P_FE2CL_REP_SEND_ANY_GROUP_MENUCHAT_MESSAGE_FAIL = 0x310000bd, // 822083773
|
||||
P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL = 0x310000be, // 822083774
|
||||
P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC = 0x310000bf, // 822083775
|
||||
P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL = 0x310000c0, // 822083776
|
||||
P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC = 0x310000c1, // 822083777
|
||||
P_FE2CL_ANNOUNCE_MSG = 0x310000c2, // 822083778
|
||||
P_FE2CL_REP_PC_SPECIAL_STATE_SWITCH_SUCC = 0x310000c3, // 822083779
|
||||
P_FE2CL_PC_SPECIAL_STATE_CHANGE = 0x310000c4, // 822083780
|
||||
P_FE2CL_GM_REP_PC_SET_VALUE = 0x310000c5, // 822083781
|
||||
P_FE2CL_GM_PC_CHANGE_VALUE = 0x310000c6, // 822083782
|
||||
P_FE2CL_GM_REP_PC_LOCATION = 0x310000c7, // 822083783
|
||||
P_FE2CL_GM_REP_PC_ANNOUNCE = 0x310000c8, // 822083784
|
||||
P_FE2CL_REP_PC_BUDDY_WARP_FAIL = 0x310000c9, // 822083785
|
||||
P_FE2CL_REP_PC_CHANGE_LEVEL = 0x310000ca, // 822083786
|
||||
P_FE2CL_REP_SET_PC_BLOCK_SUCC = 0x310000cb, // 822083787
|
||||
P_FE2CL_REP_SET_PC_BLOCK_FAIL = 0x310000cc, // 822083788
|
||||
P_FE2CL_REP_REGIST_RXCOM = 0x310000cd, // 822083789
|
||||
P_FE2CL_REP_REGIST_RXCOM_FAIL = 0x310000ce, // 822083790
|
||||
P_FE2CL_PC_INVEN_FULL_MSG = 0x310000cf, // 822083791
|
||||
P_FE2CL_REQ_LIVE_CHECK = 0x310000d0, // 822083792
|
||||
P_FE2CL_PC_MOTD_LOGIN = 0x310000d1, // 822083793
|
||||
P_FE2CL_REP_PC_ITEM_USE_FAIL = 0x310000d2, // 822083794
|
||||
P_FE2CL_REP_PC_ITEM_USE_SUCC = 0x310000d3, // 822083795
|
||||
P_FE2CL_PC_ITEM_USE = 0x310000d4, // 822083796
|
||||
P_FE2CL_REP_GET_BUDDY_LOCATION_SUCC = 0x310000d5, // 822083797
|
||||
P_FE2CL_REP_GET_BUDDY_LOCATION_FAIL = 0x310000d6, // 822083798
|
||||
P_FE2CL_REP_PC_RIDING_FAIL = 0x310000d7, // 822083799
|
||||
P_FE2CL_REP_PC_RIDING_SUCC = 0x310000d8, // 822083800
|
||||
P_FE2CL_PC_RIDING = 0x310000d9, // 822083801
|
||||
P_FE2CL_PC_BROOMSTICK_MOVE = 0x310000da, // 822083802
|
||||
P_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC = 0x310000db, // 822083803
|
||||
P_FE2CL_REP_WARP_USE_RECALL_FAIL = 0x310000dc, // 822083804
|
||||
P_FE2CL_REP_PC_EXIT_DUPLICATE = 0x310000dd, // 822083805
|
||||
P_FE2CL_REP_PC_MISSION_COMPLETE_SUCC = 0x310000de, // 822083806
|
||||
P_FE2CL_PC_BUFF_UPDATE = 0x310000df, // 822083807
|
||||
P_FE2CL_REP_PC_NEW_EMAIL = 0x310000e0, // 822083808
|
||||
P_FE2CL_REP_PC_READ_EMAIL_SUCC = 0x310000e1, // 822083809
|
||||
P_FE2CL_REP_PC_READ_EMAIL_FAIL = 0x310000e2, // 822083810
|
||||
P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_SUCC = 0x310000e3, // 822083811
|
||||
P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_FAIL = 0x310000e4, // 822083812
|
||||
P_FE2CL_REP_PC_DELETE_EMAIL_SUCC = 0x310000e5, // 822083813
|
||||
P_FE2CL_REP_PC_DELETE_EMAIL_FAIL = 0x310000e6, // 822083814
|
||||
P_FE2CL_REP_PC_SEND_EMAIL_SUCC = 0x310000e7, // 822083815
|
||||
P_FE2CL_REP_PC_SEND_EMAIL_FAIL = 0x310000e8, // 822083816
|
||||
P_FE2CL_REP_PC_RECV_EMAIL_ITEM_SUCC = 0x310000e9, // 822083817
|
||||
P_FE2CL_REP_PC_RECV_EMAIL_ITEM_FAIL = 0x310000ea, // 822083818
|
||||
P_FE2CL_REP_PC_RECV_EMAIL_CANDY_SUCC = 0x310000eb, // 822083819
|
||||
P_FE2CL_REP_PC_RECV_EMAIL_CANDY_FAIL = 0x310000ec, // 822083820
|
||||
P_FE2CL_PC_SUDDEN_DEAD = 0x310000ed, // 822083821
|
||||
P_FE2CL_REP_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF_SUCC = 0x310000ee, // 822083822
|
||||
P_FE2CL_REP_PC_SET_CURRENT_MISSION_ID = 0x310000ef, // 822083823
|
||||
P_FE2CL_REP_NPC_GROUP_INVITE_FAIL = 0x310000f0, // 822083824
|
||||
P_FE2CL_REP_NPC_GROUP_INVITE_SUCC = 0x310000f1, // 822083825
|
||||
P_FE2CL_REP_NPC_GROUP_KICK_FAIL = 0x310000f2, // 822083826
|
||||
P_FE2CL_REP_NPC_GROUP_KICK_SUCC = 0x310000f3, // 822083827
|
||||
P_FE2CL_PC_EVENT = 0x310000f4, // 822083828
|
||||
P_FE2CL_REP_PC_TRANSPORT_WARP_SUCC = 0x310000f5, // 822083829
|
||||
P_FE2CL_REP_PC_TRADE_EMOTES_CHAT_FAIL = 0x310000f6, // 822083830
|
||||
P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_SUCC = 0x310000f7, // 822083831
|
||||
P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_FAIL = 0x310000f8, // 822083832
|
||||
P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC = 0x310000f9, // 822083833
|
||||
P_FE2CL_REP_CHANNEL_INFO = 0x310000fa, // 822083834
|
||||
P_FE2CL_REP_PC_CHANNEL_NUM = 0x310000fb, // 822083835
|
||||
P_FE2CL_REP_PC_WARP_CHANNEL_FAIL = 0x310000fc, // 822083836
|
||||
P_FE2CL_REP_PC_WARP_CHANNEL_SUCC = 0x310000fd, // 822083837
|
||||
P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_SUCC = 0x310000fe, // 822083838
|
||||
P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_FAIL = 0x310000ff, // 822083839
|
||||
P_FE2CL_REP_PC_FIND_NAME_ACCEPT_BUDDY_FAIL = 0x31000100, // 822083840
|
||||
P_FE2CL_REP_PC_BUDDY_WARP_SAME_SHARD_SUCC = 0x31000101, // 822083841
|
||||
P_FE2CL_PC_ATTACK_CHARs_SUCC = 0x31000102, // 822083842
|
||||
P_FE2CL_PC_ATTACK_CHARs = 0x31000103, // 822083843
|
||||
P_FE2CL_NPC_ATTACK_CHARs = 0x31000104, // 822083844
|
||||
P_FE2CL_REP_PC_CHANGE_LEVEL_SUCC = 0x31000105, // 822083845
|
||||
P_FE2CL_REP_PC_NANO_CREATE = 0x31000106, // 822083846
|
||||
P_FE2CL_PC_STREETSTALL_REP_READY_SUCC = 0x31000107, // 822083847
|
||||
P_FE2CL_PC_STREETSTALL_REP_READY_FAIL = 0x31000108, // 822083848
|
||||
P_FE2CL_PC_STREETSTALL_REP_CANCEL_SUCC = 0x31000109, // 822083849
|
||||
P_FE2CL_PC_STREETSTALL_REP_CANCEL_FAIL = 0x3100010a, // 822083850
|
||||
P_FE2CL_PC_STREETSTALL_REP_REGIST_ITEM_SUCC = 0x3100010b, // 822083851
|
||||
P_FE2CL_PC_STREETSTALL_REP_REGIST_ITEM_FAIL = 0x3100010c, // 822083852
|
||||
P_FE2CL_PC_STREETSTALL_REP_UNREGIST_ITEM_SUCC = 0x3100010d, // 822083853
|
||||
P_FE2CL_PC_STREETSTALL_REP_UNREGIST_ITEM_FAIL = 0x3100010e, // 822083854
|
||||
P_FE2CL_PC_STREETSTALL_REP_SALE_START_SUCC = 0x3100010f, // 822083855
|
||||
P_FE2CL_PC_STREETSTALL_REP_SALE_START_FAIL = 0x31000110, // 822083856
|
||||
P_FE2CL_PC_STREETSTALL_REP_ITEM_LIST = 0x31000111, // 822083857
|
||||
P_FE2CL_PC_STREETSTALL_REP_ITEM_LIST_FAIL = 0x31000112, // 822083858
|
||||
P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_SUCC_BUYER = 0x31000113, // 822083859
|
||||
P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_SUCC_SELLER = 0x31000114, // 822083860
|
||||
P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_FAIL = 0x31000115, // 822083861
|
||||
P_FE2CL_REP_PC_ITEM_COMBINATION_SUCC = 0x31000116, // 822083862
|
||||
P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL = 0x31000117, // 822083863
|
||||
P_FE2CL_PC_CASH_BUFF_UPDATE = 0x31000118, // 822083864
|
||||
P_FE2CL_REP_PC_SKILL_ADD_SUCC = 0x31000119, // 822083865
|
||||
P_FE2CL_REP_PC_SKILL_ADD_FAIL = 0x3100011a, // 822083866
|
||||
P_FE2CL_REP_PC_SKILL_DEL_SUCC = 0x3100011b, // 822083867
|
||||
P_FE2CL_REP_PC_SKILL_DEL_FAIL = 0x3100011c, // 822083868
|
||||
P_FE2CL_REP_PC_SKILL_USE_SUCC = 0x3100011d, // 822083869
|
||||
P_FE2CL_REP_PC_SKILL_USE_FAIL = 0x3100011e, // 822083870
|
||||
P_FE2CL_PC_SKILL_USE = 0x3100011f, // 822083871
|
||||
P_FE2CL_PC_ROPE = 0x31000120, // 822083872
|
||||
P_FE2CL_PC_BELT = 0x31000121, // 822083873
|
||||
P_FE2CL_PC_VEHICLE_ON_SUCC = 0x31000122, // 822083874
|
||||
P_FE2CL_PC_VEHICLE_ON_FAIL = 0x31000123, // 822083875
|
||||
P_FE2CL_PC_VEHICLE_OFF_SUCC = 0x31000124, // 822083876
|
||||
P_FE2CL_PC_VEHICLE_OFF_FAIL = 0x31000125, // 822083877
|
||||
P_FE2CL_PC_QUICK_SLOT_INFO = 0x31000126, // 822083878
|
||||
P_FE2CL_REP_PC_REGIST_QUICK_SLOT_FAIL = 0x31000127, // 822083879
|
||||
P_FE2CL_REP_PC_REGIST_QUICK_SLOT_SUCC = 0x31000128, // 822083880
|
||||
P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM = 0x31000129, // 822083881
|
||||
P_FE2CL_REP_PC_DISASSEMBLE_ITEM_SUCC = 0x3100012a, // 822083882
|
||||
P_FE2CL_REP_PC_DISASSEMBLE_ITEM_FAIL = 0x3100012b, // 822083883
|
||||
P_FE2CL_GM_REP_REWARD_RATE_SUCC = 0x3100012c, // 822083884
|
||||
P_FE2CL_REP_PC_ITEM_ENCHANT_SUCC = 0x3100012d, // 822083885
|
||||
P_FE2CL_REP_PC_ITEM_ENCHANT_FAIL = 0x3100012e, // 822083886
|
||||
|
||||
P_LS2CL_REP_LOGIN_SUCC = 0x21000001, // 553648129
|
||||
P_LS2CL_REP_LOGIN_FAIL = 0x21000002, // 553648130
|
||||
P_LS2CL_REP_CHAR_INFO = 0x21000003, // 553648131
|
||||
P_LS2CL_REP_CHECK_CHAR_NAME_SUCC = 0x21000005, // 553648133
|
||||
P_LS2CL_REP_CHECK_CHAR_NAME_FAIL = 0x21000006, // 553648134
|
||||
P_LS2CL_REP_SAVE_CHAR_NAME_SUCC = 0x21000007, // 553648135
|
||||
P_LS2CL_REP_SAVE_CHAR_NAME_FAIL = 0x21000008, // 553648136
|
||||
P_LS2CL_REP_CHAR_CREATE_SUCC = 0x21000009, // 553648137
|
||||
P_LS2CL_REP_CHAR_CREATE_FAIL = 0x2100000a, // 553648138
|
||||
P_LS2CL_REP_CHAR_SELECT_SUCC = 0x2100000b, // 553648139
|
||||
P_LS2CL_REP_CHAR_SELECT_FAIL = 0x2100000c, // 553648140
|
||||
P_LS2CL_REP_CHAR_DELETE_SUCC = 0x2100000d, // 553648141
|
||||
P_LS2CL_REP_CHAR_DELETE_FAIL = 0x2100000e, // 553648142
|
||||
P_LS2CL_REP_SHARD_SELECT_SUCC = 0x2100000f, // 553648143
|
||||
P_LS2CL_REP_SHARD_SELECT_FAIL = 0x21000010, // 553648144
|
||||
P_LS2CL_REP_VERSION_CHECK_SUCC = 0x21000011, // 553648145
|
||||
P_LS2CL_REP_VERSION_CHECK_FAIL = 0x21000012, // 553648146
|
||||
P_LS2CL_REP_CHECK_NAME_LIST_SUCC = 0x21000013, // 553648147
|
||||
P_LS2CL_REP_CHECK_NAME_LIST_FAIL = 0x21000014, // 553648148
|
||||
P_LS2CL_REP_PC_EXIT_DUPLICATE = 0x21000015, // 553648149
|
||||
P_LS2CL_REQ_LIVE_CHECK = 0x21000016, // 553648150
|
||||
P_LS2CL_REP_CHANGE_CHAR_NAME_SUCC = 0x21000017, // 553648151
|
||||
P_LS2CL_REP_CHANGE_CHAR_NAME_FAIL = 0x21000018, // 553648152
|
||||
P_LS2CL_REP_SHARD_LIST_INFO_SUCC = 0x21000019, // 553648153
|
||||
};
|
||||
|
||||
/*
|
||||
* Numbers of packets by type.
|
||||
* Each is the last packet - the upper bits + 1
|
||||
*/
|
||||
enum {
|
||||
N_CL2LS = 0xf,
|
||||
N_CL2FE = 0xa5,
|
||||
N_FE2CL = 0x12f,
|
||||
N_LS2CL = 0x1a,
|
||||
|
||||
N_PACKETS = N_CL2LS + N_CL2FE + N_FE2CL + N_LS2CL
|
||||
};
|
||||
546
src/core/Packets.cpp
Normal file
546
src/core/Packets.cpp
Normal file
@@ -0,0 +1,546 @@
|
||||
#include <string>
|
||||
|
||||
#include "Defines.hpp"
|
||||
#include "Packets.hpp"
|
||||
#include "CNStructs.hpp"
|
||||
|
||||
#define PACKET(id) {id, {id, sizeof(s##id), #id}}
|
||||
#define MANUAL(id) {id, {id, sizeof(s##id), #id}}
|
||||
#define VAR_PACKET(id, memb, tr) {id, {id, sizeof(s##id), #id, offsetof(s##id, memb), sizeof(tr)}}
|
||||
|
||||
/*
|
||||
* This map defines descriptors for all packets, and is used by the new system
|
||||
* for validation. From now on, we have to convert new variadic packets from
|
||||
* PACKET to VAR_PACKET in this list to use them.
|
||||
*
|
||||
* MANUAL is just a form of documentation stating that the packet is variadic
|
||||
* and atypically encoded, so it won't be able to pass outbound validation and
|
||||
* will need to be manually validated and sent using the legacy sendPacket()
|
||||
* invocation pattern.
|
||||
*/
|
||||
std::map<uint32_t, PacketDesc> Packets::packets = {
|
||||
// CL2LS
|
||||
PACKET(P_CL2LS_REQ_LOGIN),
|
||||
PACKET(P_CL2LS_REQ_CHECK_CHAR_NAME),
|
||||
PACKET(P_CL2LS_REQ_SAVE_CHAR_NAME),
|
||||
PACKET(P_CL2LS_REQ_CHAR_CREATE),
|
||||
PACKET(P_CL2LS_REQ_CHAR_SELECT),
|
||||
PACKET(P_CL2LS_REQ_CHAR_DELETE),
|
||||
PACKET(P_CL2LS_REQ_SHARD_SELECT),
|
||||
PACKET(P_CL2LS_REQ_SHARD_LIST_INFO),
|
||||
PACKET(P_CL2LS_CHECK_NAME_LIST),
|
||||
PACKET(P_CL2LS_REQ_SAVE_CHAR_TUTOR),
|
||||
PACKET(P_CL2LS_REQ_PC_EXIT_DUPLICATE),
|
||||
PACKET(P_CL2LS_REP_LIVE_CHECK),
|
||||
PACKET(P_CL2LS_REQ_CHANGE_CHAR_NAME),
|
||||
PACKET(P_CL2LS_REQ_SERVER_SELECT),
|
||||
|
||||
// CL2FE
|
||||
PACKET(P_CL2FE_REQ_PC_ENTER),
|
||||
PACKET(P_CL2FE_REQ_PC_EXIT),
|
||||
PACKET(P_CL2FE_REQ_PC_MOVE),
|
||||
PACKET(P_CL2FE_REQ_PC_STOP),
|
||||
PACKET(P_CL2FE_REQ_PC_JUMP),
|
||||
VAR_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, iNPCCnt, int32_t),
|
||||
PACKET(P_CL2FE_REQ_SEND_FREECHAT_MESSAGE),
|
||||
PACKET(P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE),
|
||||
PACKET(P_CL2FE_REQ_PC_REGEN),
|
||||
PACKET(P_CL2FE_REQ_ITEM_MOVE),
|
||||
PACKET(P_CL2FE_REQ_PC_TASK_START),
|
||||
PACKET(P_CL2FE_REQ_PC_TASK_END),
|
||||
PACKET(P_CL2FE_REQ_NANO_EQUIP),
|
||||
PACKET(P_CL2FE_REQ_NANO_UNEQUIP),
|
||||
PACKET(P_CL2FE_REQ_NANO_ACTIVE),
|
||||
PACKET(P_CL2FE_REQ_NANO_TUNE),
|
||||
VAR_PACKET(P_CL2FE_REQ_NANO_SKILL_USE, iTargetCnt, int32_t),
|
||||
PACKET(P_CL2FE_REQ_PC_TASK_STOP),
|
||||
PACKET(P_CL2FE_REQ_PC_TASK_CONTINUE),
|
||||
PACKET(P_CL2FE_REQ_PC_GOTO),
|
||||
PACKET(P_CL2FE_REQ_CHARGE_NANO_STAMINA),
|
||||
PACKET(P_CL2FE_REQ_PC_KILL_QUEST_NPCs),
|
||||
PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_BUY),
|
||||
PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_SELL),
|
||||
PACKET(P_CL2FE_REQ_PC_ITEM_DELETE),
|
||||
PACKET(P_CL2FE_REQ_PC_GIVE_ITEM),
|
||||
PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_READY),
|
||||
PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE),
|
||||
VAR_PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_HIT, iTargetCnt, int64_t),
|
||||
PACKET(P_CL2FE_REQ_PC_GRENADE_STYLE_READY),
|
||||
PACKET(P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE),
|
||||
PACKET(P_CL2FE_REQ_PC_GRENADE_STYLE_HIT),
|
||||
PACKET(P_CL2FE_REQ_PC_NANO_CREATE),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_OFFER),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ABORT),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_ABORT),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_CASH_REGISTER),
|
||||
PACKET(P_CL2FE_REQ_PC_TRADE_EMOTES_CHAT),
|
||||
PACKET(P_CL2FE_REQ_PC_BANK_OPEN),
|
||||
PACKET(P_CL2FE_REQ_PC_BANK_CLOSE),
|
||||
PACKET(P_CL2FE_REQ_PC_VENDOR_START),
|
||||
PACKET(P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE),
|
||||
PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY),
|
||||
PACKET(P_CL2FE_REQ_PC_COMBAT_BEGIN),
|
||||
PACKET(P_CL2FE_REQ_PC_COMBAT_END),
|
||||
PACKET(P_CL2FE_REQ_REQUEST_MAKE_BUDDY),
|
||||
PACKET(P_CL2FE_REQ_ACCEPT_MAKE_BUDDY),
|
||||
PACKET(P_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE),
|
||||
PACKET(P_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE),
|
||||
PACKET(P_CL2FE_REQ_GET_BUDDY_STYLE),
|
||||
PACKET(P_CL2FE_REQ_SET_BUDDY_BLOCK),
|
||||
PACKET(P_CL2FE_REQ_REMOVE_BUDDY),
|
||||
PACKET(P_CL2FE_REQ_GET_BUDDY_STATE),
|
||||
PACKET(P_CL2FE_REQ_PC_JUMPPAD),
|
||||
PACKET(P_CL2FE_REQ_PC_LAUNCHER),
|
||||
PACKET(P_CL2FE_REQ_PC_ZIPLINE),
|
||||
PACKET(P_CL2FE_REQ_PC_MOVEPLATFORM),
|
||||
PACKET(P_CL2FE_REQ_PC_SLOPE),
|
||||
PACKET(P_CL2FE_REQ_PC_STATE_CHANGE),
|
||||
PACKET(P_CL2FE_REQ_PC_MAP_WARP),
|
||||
PACKET(P_CL2FE_REQ_PC_GIVE_NANO),
|
||||
PACKET(P_CL2FE_REQ_NPC_SUMMON),
|
||||
PACKET(P_CL2FE_REQ_NPC_UNSUMMON),
|
||||
PACKET(P_CL2FE_REQ_ITEM_CHEST_OPEN),
|
||||
PACKET(P_CL2FE_REQ_PC_GIVE_NANO_SKILL),
|
||||
PACKET(P_CL2FE_DOT_DAMAGE_ONOFF),
|
||||
PACKET(P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY),
|
||||
PACKET(P_CL2FE_REQ_PC_WARP_USE_NPC),
|
||||
PACKET(P_CL2FE_REQ_PC_GROUP_INVITE),
|
||||
PACKET(P_CL2FE_REQ_PC_GROUP_INVITE_REFUSE),
|
||||
PACKET(P_CL2FE_REQ_PC_GROUP_JOIN),
|
||||
PACKET(P_CL2FE_REQ_PC_GROUP_LEAVE),
|
||||
PACKET(P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT),
|
||||
PACKET(P_CL2FE_REQ_PC_BUDDY_WARP),
|
||||
PACKET(P_CL2FE_REQ_GET_MEMBER_STYLE),
|
||||
PACKET(P_CL2FE_REQ_GET_GROUP_STYLE),
|
||||
PACKET(P_CL2FE_REQ_PC_CHANGE_MENTOR),
|
||||
PACKET(P_CL2FE_REQ_GET_BUDDY_LOCATION),
|
||||
PACKET(P_CL2FE_REQ_NPC_GROUP_SUMMON),
|
||||
PACKET(P_CL2FE_REQ_PC_WARP_TO_PC),
|
||||
PACKET(P_CL2FE_REQ_EP_RANK_GET_LIST),
|
||||
PACKET(P_CL2FE_REQ_EP_RANK_GET_DETAIL),
|
||||
PACKET(P_CL2FE_REQ_EP_RANK_GET_PC_INFO),
|
||||
PACKET(P_CL2FE_REQ_EP_RACE_START),
|
||||
PACKET(P_CL2FE_REQ_EP_RACE_END),
|
||||
PACKET(P_CL2FE_REQ_EP_RACE_CANCEL),
|
||||
PACKET(P_CL2FE_REQ_EP_GET_RING),
|
||||
PACKET(P_CL2FE_REQ_IM_CHANGE_SWITCH_STATUS),
|
||||
PACKET(P_CL2FE_REQ_SHINY_PICKUP),
|
||||
PACKET(P_CL2FE_REQ_SHINY_SUMMON),
|
||||
PACKET(P_CL2FE_REQ_PC_MOVETRANSPORTATION),
|
||||
PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE),
|
||||
PACKET(P_CL2FE_REQ_SEND_ANY_GROUP_FREECHAT_MESSAGE),
|
||||
PACKET(P_CL2FE_REQ_BARKER),
|
||||
PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE),
|
||||
PACKET(P_CL2FE_REQ_SEND_ANY_GROUP_MENUCHAT_MESSAGE),
|
||||
PACKET(P_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION),
|
||||
PACKET(P_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION),
|
||||
PACKET(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH),
|
||||
PACKET(P_CL2FE_GM_REQ_PC_SET_VALUE),
|
||||
PACKET(P_CL2FE_GM_REQ_KICK_PLAYER),
|
||||
PACKET(P_CL2FE_GM_REQ_TARGET_PC_TELEPORT),
|
||||
PACKET(P_CL2FE_GM_REQ_PC_LOCATION),
|
||||
PACKET(P_CL2FE_GM_REQ_PC_ANNOUNCE),
|
||||
PACKET(P_CL2FE_REQ_SET_PC_BLOCK),
|
||||
PACKET(P_CL2FE_REQ_REGIST_RXCOM),
|
||||
PACKET(P_CL2FE_GM_REQ_PC_MOTD_REGISTER),
|
||||
PACKET(P_CL2FE_REQ_ITEM_USE),
|
||||
PACKET(P_CL2FE_REQ_WARP_USE_RECALL),
|
||||
PACKET(P_CL2FE_REP_LIVE_CHECK),
|
||||
PACKET(P_CL2FE_REQ_PC_MISSION_COMPLETE),
|
||||
PACKET(P_CL2FE_REQ_PC_TASK_COMPLETE),
|
||||
PACKET(P_CL2FE_REQ_NPC_INTERACTION),
|
||||
PACKET(P_CL2FE_DOT_HEAL_ONOFF),
|
||||
PACKET(P_CL2FE_REQ_PC_SPECIAL_STATE_SWITCH),
|
||||
PACKET(P_CL2FE_REQ_PC_EMAIL_UPDATE_CHECK),
|
||||
PACKET(P_CL2FE_REQ_PC_READ_EMAIL),
|
||||
PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST),
|
||||
PACKET(P_CL2FE_REQ_PC_DELETE_EMAIL),
|
||||
PACKET(P_CL2FE_REQ_PC_SEND_EMAIL),
|
||||
PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM),
|
||||
PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_CANDY),
|
||||
PACKET(P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF),
|
||||
PACKET(P_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID),
|
||||
PACKET(P_CL2FE_REQ_NPC_GROUP_INVITE),
|
||||
PACKET(P_CL2FE_REQ_NPC_GROUP_KICK),
|
||||
PACKET(P_CL2FE_REQ_PC_FIRST_USE_FLAG_SET),
|
||||
PACKET(P_CL2FE_REQ_PC_TRANSPORT_WARP),
|
||||
PACKET(P_CL2FE_REQ_PC_TIME_TO_GO_WARP),
|
||||
PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL),
|
||||
PACKET(P_CL2FE_REQ_CHANNEL_INFO),
|
||||
PACKET(P_CL2FE_REQ_PC_CHANNEL_NUM),
|
||||
PACKET(P_CL2FE_REQ_PC_WARP_CHANNEL),
|
||||
PACKET(P_CL2FE_REQ_PC_LOADING_COMPLETE),
|
||||
PACKET(P_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY),
|
||||
PACKET(P_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY),
|
||||
VAR_PACKET(P_CL2FE_REQ_PC_ATTACK_CHARs, iTargetCnt, sGM_PVPTarget),
|
||||
PACKET(P_CL2FE_PC_STREETSTALL_REQ_READY),
|
||||
PACKET(P_CL2FE_PC_STREETSTALL_REQ_CANCEL),
|
||||
PACKET(P_CL2FE_PC_STREETSTALL_REQ_REGIST_ITEM),
|
||||
PACKET(P_CL2FE_PC_STREETSTALL_REQ_UNREGIST_ITEM),
|
||||
PACKET(P_CL2FE_PC_STREETSTALL_REQ_SALE_START),
|
||||
PACKET(P_CL2FE_PC_STREETSTALL_REQ_ITEM_LIST),
|
||||
PACKET(P_CL2FE_PC_STREETSTALL_REQ_ITEM_BUY),
|
||||
PACKET(P_CL2FE_REQ_PC_ITEM_COMBINATION),
|
||||
PACKET(P_CL2FE_GM_REQ_SET_PC_SKILL),
|
||||
PACKET(P_CL2FE_REQ_PC_SKILL_ADD),
|
||||
PACKET(P_CL2FE_REQ_PC_SKILL_DEL),
|
||||
PACKET(P_CL2FE_REQ_PC_SKILL_USE),
|
||||
PACKET(P_CL2FE_REQ_PC_ROPE),
|
||||
PACKET(P_CL2FE_REQ_PC_BELT),
|
||||
PACKET(P_CL2FE_REQ_PC_VEHICLE_ON),
|
||||
PACKET(P_CL2FE_REQ_PC_VEHICLE_OFF),
|
||||
PACKET(P_CL2FE_REQ_PC_REGIST_QUICK_SLOT),
|
||||
PACKET(P_CL2FE_REQ_PC_DISASSEMBLE_ITEM),
|
||||
PACKET(P_CL2FE_GM_REQ_REWARD_RATE),
|
||||
PACKET(P_CL2FE_REQ_PC_ITEM_ENCHANT),
|
||||
|
||||
// LS2CL
|
||||
PACKET(P_LS2CL_REP_LOGIN_SUCC),
|
||||
PACKET(P_LS2CL_REP_LOGIN_FAIL),
|
||||
PACKET(P_LS2CL_REP_CHAR_INFO),
|
||||
PACKET(P_LS2CL_REP_CHECK_CHAR_NAME_SUCC),
|
||||
PACKET(P_LS2CL_REP_CHECK_CHAR_NAME_FAIL),
|
||||
PACKET(P_LS2CL_REP_SAVE_CHAR_NAME_SUCC),
|
||||
PACKET(P_LS2CL_REP_SAVE_CHAR_NAME_FAIL),
|
||||
PACKET(P_LS2CL_REP_CHAR_CREATE_SUCC),
|
||||
PACKET(P_LS2CL_REP_CHAR_CREATE_FAIL),
|
||||
PACKET(P_LS2CL_REP_CHAR_SELECT_SUCC),
|
||||
PACKET(P_LS2CL_REP_CHAR_SELECT_FAIL),
|
||||
PACKET(P_LS2CL_REP_CHAR_DELETE_SUCC),
|
||||
PACKET(P_LS2CL_REP_CHAR_DELETE_FAIL),
|
||||
PACKET(P_LS2CL_REP_SHARD_SELECT_SUCC),
|
||||
PACKET(P_LS2CL_REP_SHARD_SELECT_FAIL),
|
||||
PACKET(P_LS2CL_REP_VERSION_CHECK_SUCC),
|
||||
PACKET(P_LS2CL_REP_VERSION_CHECK_FAIL),
|
||||
PACKET(P_LS2CL_REP_CHECK_NAME_LIST_SUCC),
|
||||
PACKET(P_LS2CL_REP_CHECK_NAME_LIST_FAIL),
|
||||
PACKET(P_LS2CL_REP_PC_EXIT_DUPLICATE),
|
||||
PACKET(P_LS2CL_REQ_LIVE_CHECK),
|
||||
PACKET(P_LS2CL_REP_CHANGE_CHAR_NAME_SUCC),
|
||||
PACKET(P_LS2CL_REP_CHANGE_CHAR_NAME_FAIL),
|
||||
PACKET(P_LS2CL_REP_SHARD_LIST_INFO_SUCC),
|
||||
|
||||
// FE2CL
|
||||
PACKET(P_FE2CL_ERROR),
|
||||
PACKET(P_FE2CL_REP_PC_ENTER_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_ENTER_SUCC),
|
||||
PACKET(P_FE2CL_PC_NEW),
|
||||
PACKET(P_FE2CL_REP_PC_EXIT_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_EXIT_SUCC),
|
||||
PACKET(P_FE2CL_PC_EXIT),
|
||||
PACKET(P_FE2CL_PC_AROUND),
|
||||
PACKET(P_FE2CL_PC_MOVE),
|
||||
PACKET(P_FE2CL_PC_STOP),
|
||||
PACKET(P_FE2CL_PC_JUMP),
|
||||
PACKET(P_FE2CL_NPC_ENTER),
|
||||
PACKET(P_FE2CL_NPC_EXIT),
|
||||
PACKET(P_FE2CL_NPC_MOVE),
|
||||
PACKET(P_FE2CL_NPC_NEW),
|
||||
PACKET(P_FE2CL_NPC_AROUND),
|
||||
PACKET(P_FE2CL_AROUND_DEL_PC),
|
||||
PACKET(P_FE2CL_AROUND_DEL_NPC),
|
||||
PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC),
|
||||
PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_FAIL),
|
||||
VAR_PACKET(P_FE2CL_PC_ATTACK_NPCs_SUCC, iNPCCnt, sAttackResult),
|
||||
VAR_PACKET(P_FE2CL_PC_ATTACK_NPCs, iNPCCnt, sAttackResult),
|
||||
VAR_PACKET(P_FE2CL_NPC_ATTACK_PCs, iPCCnt, sAttackResult),
|
||||
PACKET(P_FE2CL_REP_PC_REGEN_SUCC),
|
||||
PACKET(P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC),
|
||||
PACKET(P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_FAIL),
|
||||
PACKET(P_FE2CL_PC_ITEM_MOVE_SUCC),
|
||||
PACKET(P_FE2CL_PC_EQUIP_CHANGE),
|
||||
PACKET(P_FE2CL_REP_PC_TASK_START_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_TASK_START_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_TASK_END_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_TASK_END_FAIL),
|
||||
PACKET(P_FE2CL_NPC_SKILL_READY),
|
||||
PACKET(P_FE2CL_NPC_SKILL_FIRE),
|
||||
MANUAL(P_FE2CL_NPC_SKILL_HIT), // variadic, trailer type depends on power
|
||||
PACKET(P_FE2CL_NPC_SKILL_CORRUPTION_READY),
|
||||
VAR_PACKET(P_FE2CL_NPC_SKILL_CORRUPTION_HIT, iTargetCnt, sCAttackResult),
|
||||
PACKET(P_FE2CL_NPC_SKILL_CANCEL),
|
||||
PACKET(P_FE2CL_REP_NANO_EQUIP_SUCC),
|
||||
PACKET(P_FE2CL_REP_NANO_UNEQUIP_SUCC),
|
||||
PACKET(P_FE2CL_REP_NANO_ACTIVE_SUCC),
|
||||
PACKET(P_FE2CL_REP_NANO_TUNE_SUCC),
|
||||
PACKET(P_FE2CL_NANO_ACTIVE),
|
||||
MANUAL(P_FE2CL_NANO_SKILL_USE_SUCC), // variadic, trailer type depends on power
|
||||
PACKET(P_FE2CL_NANO_SKILL_USE),
|
||||
PACKET(P_FE2CL_REP_PC_TASK_STOP_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_TASK_STOP_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_TASK_CONTINUE_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_TASK_CONTINUE_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_GOTO_SUCC),
|
||||
PACKET(P_FE2CL_REP_CHARGE_NANO_STAMINA),
|
||||
PACKET(P_FE2CL_REP_PC_TICK),
|
||||
PACKET(P_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_ITEM_DELETE_SUCC),
|
||||
PACKET(P_FE2CL_PC_ROCKET_STYLE_READY),
|
||||
PACKET(P_FE2CL_REP_PC_ROCKET_STYLE_FIRE_SUCC),
|
||||
PACKET(P_FE2CL_PC_ROCKET_STYLE_FIRE),
|
||||
PACKET(P_FE2CL_PC_ROCKET_STYLE_HIT),
|
||||
PACKET(P_FE2CL_PC_GRENADE_STYLE_READY),
|
||||
PACKET(P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC),
|
||||
PACKET(P_FE2CL_PC_GRENADE_STYLE_FIRE),
|
||||
VAR_PACKET(P_FE2CL_PC_GRENADE_STYLE_HIT, iTargetCnt, sAttackResult),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_OFFER),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_OFFER_CANCEL),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_OFFER_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_OFFER_ABORT),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_CONFIRM),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_CONFIRM_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_CASH_REGISTER_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_EMOTES_CHAT),
|
||||
PACKET(P_FE2CL_REP_PC_NANO_CREATE_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_NANO_CREATE_FAIL),
|
||||
PACKET(P_FE2CL_REP_NANO_TUNE_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_BANK_OPEN_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_BANK_OPEN_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_BANK_CLOSE_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_BANK_CLOSE_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_START_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_START_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL),
|
||||
PACKET(P_FE2CL_CHAR_TIME_BUFF_TIME_OUT),
|
||||
PACKET(P_FE2CL_REP_PC_GIVE_ITEM_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_GIVE_ITEM_FAIL),
|
||||
VAR_PACKET(P_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC, iBuddyCnt, sBuddyBaseInfo),
|
||||
PACKET(P_FE2CL_REP_PC_BUDDYLIST_INFO_FAIL),
|
||||
PACKET(P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC),
|
||||
PACKET(P_FE2CL_REP_REQUEST_MAKE_BUDDY_FAIL),
|
||||
PACKET(P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC),
|
||||
PACKET(P_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL),
|
||||
PACKET(P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC),
|
||||
PACKET(P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_FAIL),
|
||||
PACKET(P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC),
|
||||
PACKET(P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_FAIL),
|
||||
PACKET(P_FE2CL_REP_GET_BUDDY_STYLE_SUCC),
|
||||
PACKET(P_FE2CL_REP_GET_BUDDY_STYLE_FAIL),
|
||||
PACKET(P_FE2CL_REP_GET_BUDDY_STATE_SUCC),
|
||||
PACKET(P_FE2CL_REP_GET_BUDDY_STATE_FAIL),
|
||||
PACKET(P_FE2CL_REP_SET_BUDDY_BLOCK_SUCC),
|
||||
PACKET(P_FE2CL_REP_SET_BUDDY_BLOCK_FAIL),
|
||||
PACKET(P_FE2CL_REP_REMOVE_BUDDY_SUCC),
|
||||
PACKET(P_FE2CL_REP_REMOVE_BUDDY_FAIL),
|
||||
PACKET(P_FE2CL_PC_JUMPPAD),
|
||||
PACKET(P_FE2CL_PC_LAUNCHER),
|
||||
PACKET(P_FE2CL_PC_ZIPLINE),
|
||||
PACKET(P_FE2CL_PC_MOVEPLATFORM),
|
||||
PACKET(P_FE2CL_PC_SLOPE),
|
||||
PACKET(P_FE2CL_PC_STATE_CHANGE),
|
||||
PACKET(P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC_TO_ACCEPTER),
|
||||
VAR_PACKET(P_FE2CL_REP_REWARD_ITEM, iItemCnt, sItemReward),
|
||||
PACKET(P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC),
|
||||
PACKET(P_FE2CL_REP_ITEM_CHEST_OPEN_FAIL),
|
||||
MANUAL(P_FE2CL_CHAR_TIME_BUFF_TIME_TICK), // variadic, depends on skill type
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL),
|
||||
PACKET(P_FE2CL_NPC_ROCKET_STYLE_FIRE),
|
||||
PACKET(P_FE2CL_NPC_GRENADE_STYLE_FIRE),
|
||||
PACKET(P_FE2CL_NPC_BULLET_STYLE_HIT),
|
||||
PACKET(P_FE2CL_CHARACTER_ATTACK_CHARACTERs),
|
||||
PACKET(P_FE2CL_PC_GROUP_INVITE),
|
||||
PACKET(P_FE2CL_PC_GROUP_INVITE_FAIL),
|
||||
PACKET(P_FE2CL_PC_GROUP_INVITE_REFUSE),
|
||||
MANUAL(P_FE2CL_PC_GROUP_JOIN), // double-variadic, incompatible with this system
|
||||
PACKET(P_FE2CL_PC_GROUP_JOIN_FAIL),
|
||||
PACKET(P_FE2CL_PC_GROUP_JOIN_SUCC), // probably these ones too, but we don't use them anyway
|
||||
MANUAL(P_FE2CL_PC_GROUP_LEAVE), // double-variadic, incompatible with this system
|
||||
PACKET(P_FE2CL_PC_GROUP_LEAVE_FAIL),
|
||||
PACKET(P_FE2CL_PC_GROUP_LEAVE_SUCC), // see GROUP_JOIN_SUCC
|
||||
MANUAL(P_FE2CL_PC_GROUP_MEMBER_INFO), // double-variadic, incompatible with this system
|
||||
PACKET(P_FE2CL_REP_PC_WARP_USE_NPC_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_WARP_USE_NPC_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT),
|
||||
PACKET(P_FE2CL_REP_PC_CHANGE_MENTOR_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_CHANGE_MENTOR_FAIL),
|
||||
PACKET(P_FE2CL_REP_GET_MEMBER_STYLE_FAIL),
|
||||
PACKET(P_FE2CL_REP_GET_MEMBER_STYLE_SUCC),
|
||||
PACKET(P_FE2CL_REP_GET_GROUP_STYLE_FAIL),
|
||||
PACKET(P_FE2CL_REP_GET_GROUP_STYLE_SUCC),
|
||||
PACKET(P_FE2CL_PC_REGEN),
|
||||
PACKET(P_FE2CL_INSTANCE_MAP_INFO),
|
||||
PACKET(P_FE2CL_TRANSPORTATION_ENTER),
|
||||
PACKET(P_FE2CL_TRANSPORTATION_EXIT),
|
||||
PACKET(P_FE2CL_TRANSPORTATION_MOVE),
|
||||
PACKET(P_FE2CL_TRANSPORTATION_NEW),
|
||||
PACKET(P_FE2CL_TRANSPORTATION_AROUND),
|
||||
PACKET(P_FE2CL_AROUND_DEL_TRANSPORTATION),
|
||||
PACKET(P_FE2CL_REP_EP_RANK_LIST),
|
||||
PACKET(P_FE2CL_REP_EP_RANK_DETAIL),
|
||||
PACKET(P_FE2CL_REP_EP_RANK_PC_INFO),
|
||||
PACKET(P_FE2CL_REP_EP_RACE_START_SUCC),
|
||||
PACKET(P_FE2CL_REP_EP_RACE_START_FAIL),
|
||||
PACKET(P_FE2CL_REP_EP_RACE_END_SUCC),
|
||||
PACKET(P_FE2CL_REP_EP_RACE_END_FAIL),
|
||||
PACKET(P_FE2CL_REP_EP_RACE_CANCEL_SUCC),
|
||||
PACKET(P_FE2CL_REP_EP_RACE_CANCEL_FAIL),
|
||||
PACKET(P_FE2CL_REP_EP_GET_RING_SUCC),
|
||||
PACKET(P_FE2CL_REP_EP_GET_RING_FAIL),
|
||||
PACKET(P_FE2CL_REP_IM_CHANGE_SWITCH_STATUS),
|
||||
PACKET(P_FE2CL_SHINY_ENTER),
|
||||
PACKET(P_FE2CL_SHINY_EXIT),
|
||||
PACKET(P_FE2CL_SHINY_NEW),
|
||||
PACKET(P_FE2CL_SHINY_AROUND),
|
||||
PACKET(P_FE2CL_AROUND_DEL_SHINY),
|
||||
PACKET(P_FE2CL_REP_SHINY_PICKUP_FAIL),
|
||||
PACKET(P_FE2CL_REP_SHINY_PICKUP_SUCC),
|
||||
PACKET(P_FE2CL_PC_MOVETRANSPORTATION),
|
||||
PACKET(P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC),
|
||||
PACKET(P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_FAIL),
|
||||
PACKET(P_FE2CL_REP_SEND_ANY_GROUP_FREECHAT_MESSAGE_SUCC),
|
||||
PACKET(P_FE2CL_REP_SEND_ANY_GROUP_FREECHAT_MESSAGE_FAIL),
|
||||
PACKET(P_FE2CL_REP_BARKER),
|
||||
PACKET(P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC),
|
||||
PACKET(P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_FAIL),
|
||||
PACKET(P_FE2CL_REP_SEND_ANY_GROUP_MENUCHAT_MESSAGE_SUCC),
|
||||
PACKET(P_FE2CL_REP_SEND_ANY_GROUP_MENUCHAT_MESSAGE_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC),
|
||||
PACKET(P_FE2CL_ANNOUNCE_MSG),
|
||||
PACKET(P_FE2CL_REP_PC_SPECIAL_STATE_SWITCH_SUCC),
|
||||
PACKET(P_FE2CL_PC_SPECIAL_STATE_CHANGE),
|
||||
PACKET(P_FE2CL_GM_REP_PC_SET_VALUE),
|
||||
PACKET(P_FE2CL_GM_PC_CHANGE_VALUE),
|
||||
PACKET(P_FE2CL_GM_REP_PC_LOCATION),
|
||||
PACKET(P_FE2CL_GM_REP_PC_ANNOUNCE),
|
||||
PACKET(P_FE2CL_REP_PC_BUDDY_WARP_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_CHANGE_LEVEL),
|
||||
PACKET(P_FE2CL_REP_SET_PC_BLOCK_SUCC),
|
||||
PACKET(P_FE2CL_REP_SET_PC_BLOCK_FAIL),
|
||||
PACKET(P_FE2CL_REP_REGIST_RXCOM),
|
||||
PACKET(P_FE2CL_REP_REGIST_RXCOM_FAIL),
|
||||
PACKET(P_FE2CL_PC_INVEN_FULL_MSG),
|
||||
PACKET(P_FE2CL_REQ_LIVE_CHECK),
|
||||
PACKET(P_FE2CL_PC_MOTD_LOGIN),
|
||||
PACKET(P_FE2CL_REP_PC_ITEM_USE_FAIL),
|
||||
VAR_PACKET(P_FE2CL_REP_PC_ITEM_USE_SUCC, iTargetCnt, sSkillResult_Buff),
|
||||
PACKET(P_FE2CL_PC_ITEM_USE),
|
||||
PACKET(P_FE2CL_REP_GET_BUDDY_LOCATION_SUCC),
|
||||
PACKET(P_FE2CL_REP_GET_BUDDY_LOCATION_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_RIDING_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_RIDING_SUCC),
|
||||
PACKET(P_FE2CL_PC_RIDING),
|
||||
PACKET(P_FE2CL_PC_BROOMSTICK_MOVE),
|
||||
PACKET(P_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC),
|
||||
PACKET(P_FE2CL_REP_WARP_USE_RECALL_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_EXIT_DUPLICATE),
|
||||
PACKET(P_FE2CL_REP_PC_MISSION_COMPLETE_SUCC),
|
||||
PACKET(P_FE2CL_PC_BUFF_UPDATE),
|
||||
PACKET(P_FE2CL_REP_PC_NEW_EMAIL),
|
||||
PACKET(P_FE2CL_REP_PC_READ_EMAIL_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_READ_EMAIL_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_DELETE_EMAIL_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_DELETE_EMAIL_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_SEND_EMAIL_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_SEND_EMAIL_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_ITEM_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_ITEM_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_CANDY_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_CANDY_FAIL),
|
||||
PACKET(P_FE2CL_PC_SUDDEN_DEAD),
|
||||
PACKET(P_FE2CL_REP_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_SET_CURRENT_MISSION_ID),
|
||||
PACKET(P_FE2CL_REP_NPC_GROUP_INVITE_FAIL),
|
||||
PACKET(P_FE2CL_REP_NPC_GROUP_INVITE_SUCC),
|
||||
PACKET(P_FE2CL_REP_NPC_GROUP_KICK_FAIL),
|
||||
PACKET(P_FE2CL_REP_NPC_GROUP_KICK_SUCC),
|
||||
PACKET(P_FE2CL_PC_EVENT),
|
||||
PACKET(P_FE2CL_REP_PC_TRANSPORT_WARP_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_TRADE_EMOTES_CHAT_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC),
|
||||
PACKET(P_FE2CL_REP_CHANNEL_INFO),
|
||||
PACKET(P_FE2CL_REP_PC_CHANNEL_NUM),
|
||||
PACKET(P_FE2CL_REP_PC_WARP_CHANNEL_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_WARP_CHANNEL_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_FIND_NAME_ACCEPT_BUDDY_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_BUDDY_WARP_SAME_SHARD_SUCC),
|
||||
VAR_PACKET(P_FE2CL_PC_ATTACK_CHARs_SUCC, iTargetCnt, sAttackResult),
|
||||
VAR_PACKET(P_FE2CL_PC_ATTACK_CHARs, iTargetCnt, sAttackResult),
|
||||
PACKET(P_FE2CL_NPC_ATTACK_CHARs),
|
||||
PACKET(P_FE2CL_REP_PC_CHANGE_LEVEL_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_NANO_CREATE),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_READY_SUCC),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_READY_FAIL),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_CANCEL_SUCC),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_CANCEL_FAIL),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_REGIST_ITEM_SUCC),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_REGIST_ITEM_FAIL),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_UNREGIST_ITEM_SUCC),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_UNREGIST_ITEM_FAIL),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_SALE_START_SUCC),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_SALE_START_FAIL),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_ITEM_LIST),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_ITEM_LIST_FAIL),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_SUCC_BUYER),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_SUCC_SELLER),
|
||||
PACKET(P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_ITEM_COMBINATION_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL),
|
||||
PACKET(P_FE2CL_PC_CASH_BUFF_UPDATE),
|
||||
PACKET(P_FE2CL_REP_PC_SKILL_ADD_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_SKILL_ADD_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_SKILL_DEL_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_SKILL_DEL_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_SKILL_USE_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_SKILL_USE_FAIL),
|
||||
PACKET(P_FE2CL_PC_SKILL_USE),
|
||||
PACKET(P_FE2CL_PC_ROPE),
|
||||
PACKET(P_FE2CL_PC_BELT),
|
||||
PACKET(P_FE2CL_PC_VEHICLE_ON_SUCC),
|
||||
PACKET(P_FE2CL_PC_VEHICLE_ON_FAIL),
|
||||
PACKET(P_FE2CL_PC_VEHICLE_OFF_SUCC),
|
||||
PACKET(P_FE2CL_PC_VEHICLE_OFF_FAIL),
|
||||
PACKET(P_FE2CL_PC_QUICK_SLOT_INFO),
|
||||
PACKET(P_FE2CL_REP_PC_REGIST_QUICK_SLOT_FAIL),
|
||||
PACKET(P_FE2CL_REP_PC_REGIST_QUICK_SLOT_SUCC),
|
||||
VAR_PACKET(P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, iItemListCount, sTimeLimitItemDeleteInfo2CL),
|
||||
PACKET(P_FE2CL_REP_PC_DISASSEMBLE_ITEM_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_DISASSEMBLE_ITEM_FAIL),
|
||||
PACKET(P_FE2CL_GM_REP_REWARD_RATE_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_ITEM_ENCHANT_SUCC),
|
||||
PACKET(P_FE2CL_REP_PC_ITEM_ENCHANT_FAIL),
|
||||
|
||||
#ifdef ACADEMY
|
||||
// Academy-specific
|
||||
PACKET(P_FE2CL_REP_NANO_BOOK_SUBSET),
|
||||
#endif
|
||||
};
|
||||
|
||||
std::string Packets::p2str(int val) {
|
||||
if (packets.find(val) == packets.end())
|
||||
return "UNKNOWN";
|
||||
|
||||
return packets[val].name;
|
||||
}
|
||||
63
src/core/Packets.hpp
Normal file
63
src/core/Packets.hpp
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "CNStructs.hpp"
|
||||
|
||||
#include <map>
|
||||
|
||||
// Packet Descriptor
|
||||
struct PacketDesc {
|
||||
uint32_t val;
|
||||
std::string name;
|
||||
size_t size;
|
||||
bool variadic;
|
||||
size_t cntMembOfs;
|
||||
size_t trailerSize;
|
||||
|
||||
PacketDesc() {}
|
||||
|
||||
PacketDesc(const PacketDesc& other) {
|
||||
val = other.val;
|
||||
name = other.name;
|
||||
size = other.size;
|
||||
variadic = other.variadic;
|
||||
cntMembOfs = other.cntMembOfs;
|
||||
trailerSize = other.trailerSize;
|
||||
}
|
||||
|
||||
PacketDesc(PacketDesc&& other) {
|
||||
val = other.val;
|
||||
name = std::move(other.name);
|
||||
size = other.size;
|
||||
variadic = other.variadic;
|
||||
cntMembOfs = other.cntMembOfs;
|
||||
trailerSize = other.trailerSize;
|
||||
}
|
||||
|
||||
// non-variadic constructor
|
||||
PacketDesc(uint32_t v, size_t s, std::string n) :
|
||||
val(v), name(n), size(s), variadic(false) {}
|
||||
|
||||
// variadic constructor
|
||||
PacketDesc(uint32_t v, size_t s, std::string n, size_t ofs, size_t ts) :
|
||||
val(v), name(n), size(s), variadic(true), cntMembOfs(ofs), trailerSize(ts) {}
|
||||
};
|
||||
|
||||
/*
|
||||
* Extra trailer structs for places where the client doesn't have any, but
|
||||
* really should.
|
||||
*/
|
||||
struct sGM_PVPTarget {
|
||||
uint32_t eCT;
|
||||
uint32_t iID;
|
||||
};
|
||||
|
||||
struct sSkillResult_Leech {
|
||||
sSkillResult_Heal_HP Heal;
|
||||
sSkillResult_Damage Damage;
|
||||
};
|
||||
|
||||
namespace Packets {
|
||||
extern std::map<uint32_t, PacketDesc> packets;
|
||||
|
||||
std::string p2str(int val);
|
||||
}
|
||||
114
src/db/Database.hpp
Normal file
114
src/db/Database.hpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include "Player.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define DATABASE_VERSION 4
|
||||
|
||||
namespace Database {
|
||||
|
||||
struct Account {
|
||||
int AccountID;
|
||||
std::string Password;
|
||||
int Selected;
|
||||
time_t BannedUntil;
|
||||
std::string BanReason;
|
||||
};
|
||||
|
||||
struct EmailData {
|
||||
int PlayerId;
|
||||
int MsgIndex;
|
||||
int32_t ReadFlag;
|
||||
int32_t ItemFlag;
|
||||
int SenderId;
|
||||
std::string SenderFirstName;
|
||||
std::string SenderLastName;
|
||||
std::string SubjectLine;
|
||||
std::string MsgBody;
|
||||
int Taros;
|
||||
uint64_t SendTime;
|
||||
uint64_t DeleteTime;
|
||||
};
|
||||
|
||||
struct RaceRanking {
|
||||
int EPID;
|
||||
int PlayerID;
|
||||
int Score;
|
||||
int RingCount;
|
||||
uint64_t Time;
|
||||
uint64_t Timestamp;
|
||||
};
|
||||
|
||||
void init();
|
||||
void open();
|
||||
void close();
|
||||
|
||||
void findAccount(Account* account, std::string login);
|
||||
// returns ID, 0 if something failed
|
||||
int addAccount(std::string login, std::string password);
|
||||
|
||||
// interface for the /ban command
|
||||
bool banPlayer(int playerId, std::string& reason);
|
||||
bool unbanPlayer(int playerId);
|
||||
|
||||
void updateSelected(int accountId, int slot);
|
||||
void updateSelectedByPlayerId(int accountId, int playerId);
|
||||
|
||||
bool validateCharacter(int characterID, int userID);
|
||||
bool isNameFree(std::string firstName, std::string lastName);
|
||||
bool isSlotFree(int accountId, int slotNum);
|
||||
/// returns ID, 0 if something failed
|
||||
int createCharacter(sP_CL2LS_REQ_SAVE_CHAR_NAME* save, int AccountID);
|
||||
/// returns true if query succeeded
|
||||
bool finishCharacter(sP_CL2LS_REQ_CHAR_CREATE* character, int accountId);
|
||||
/// returns true if query succeeded
|
||||
bool finishTutorial(int playerID, int accountID);
|
||||
/// returns slot number if query succeeded
|
||||
int deleteCharacter(int characterID, int userID);
|
||||
void getCharInfo(std::vector <sP_LS2CL_REP_CHAR_INFO>* result, int userID);
|
||||
|
||||
/// accepting/declining custom name
|
||||
enum class CustomName {
|
||||
APPROVE = 1,
|
||||
DISAPPROVE = 2
|
||||
};
|
||||
void evaluateCustomName(int characterID, CustomName decision);
|
||||
/// returns true if query succeeded
|
||||
bool changeName(sP_CL2LS_REQ_CHANGE_CHAR_NAME* save, int accountId);
|
||||
|
||||
// getting players
|
||||
void getPlayer(Player* plr, int id);
|
||||
bool _updatePlayer(Player *player);
|
||||
void updatePlayer(Player *player);
|
||||
void commitTrade(Player *plr1, Player *plr2);
|
||||
|
||||
// buddies
|
||||
int getNumBuddies(Player* player);
|
||||
void addBuddyship(int playerA, int playerB);
|
||||
void removeBuddyship(int playerA, int playerB);
|
||||
|
||||
// blocking
|
||||
void addBlock(int playerId, int blockedPlayerId);
|
||||
void removeBlock(int playerId, int blockedPlayerId);
|
||||
|
||||
// email
|
||||
int getUnreadEmailCount(int playerID);
|
||||
std::vector<EmailData> getEmails(int playerID, int page);
|
||||
EmailData getEmail(int playerID, int index);
|
||||
sItemBase* getEmailAttachments(int playerID, int index);
|
||||
void updateEmailContent(EmailData* data);
|
||||
void deleteEmailAttachments(int playerID, int index, int slot);
|
||||
void deleteEmails(int playerID, int64_t* indices);
|
||||
int getNextEmailIndex(int playerID);
|
||||
bool sendEmail(EmailData* data, std::vector<sItemBase> attachments, Player *sender);
|
||||
|
||||
// racing
|
||||
RaceRanking getTopRaceRanking(int epID, int playerID);
|
||||
void postRaceRanking(RaceRanking ranking);
|
||||
|
||||
// code items
|
||||
bool isCodeRedeemed(int playerId, std::string code);
|
||||
void recordCodeRedemption(int playerId, std::string code);
|
||||
}
|
||||
343
src/db/email.cpp
Normal file
343
src/db/email.cpp
Normal file
@@ -0,0 +1,343 @@
|
||||
#include "db/internal.hpp"
|
||||
|
||||
// Email-related DB interactions
|
||||
|
||||
int Database::getUnreadEmailCount(int playerID) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT COUNT(*) FROM EmailData
|
||||
WHERE PlayerID = ? AND ReadFlag = 0;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, playerID);
|
||||
sqlite3_step(stmt);
|
||||
int ret = sqlite3_column_int(stmt, 0);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<EmailData> Database::getEmails(int playerID, int page) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
std::vector<EmailData> emails;
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT
|
||||
MsgIndex, ItemFlag, ReadFlag, SenderID,
|
||||
SenderFirstName, SenderLastName, SubjectLine,
|
||||
MsgBody, Taros, SendTime, DeleteTime
|
||||
FROM EmailData
|
||||
WHERE PlayerID = ?
|
||||
ORDER BY MsgIndex DESC
|
||||
LIMIT 5
|
||||
OFFSET ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, playerID);
|
||||
int offset = 5 * page - 5;
|
||||
sqlite3_bind_int(stmt, 2, offset);
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
EmailData toAdd;
|
||||
toAdd.PlayerId = playerID;
|
||||
toAdd.MsgIndex = sqlite3_column_int(stmt, 0);
|
||||
toAdd.ItemFlag = sqlite3_column_int(stmt, 1);
|
||||
toAdd.ReadFlag = sqlite3_column_int(stmt, 2);
|
||||
toAdd.SenderId = sqlite3_column_int(stmt, 3);
|
||||
toAdd.SenderFirstName = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4)));
|
||||
toAdd.SenderLastName = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 5)));
|
||||
toAdd.SubjectLine = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6)));
|
||||
toAdd.MsgBody = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 7)));
|
||||
toAdd.Taros = sqlite3_column_int(stmt, 8);
|
||||
toAdd.SendTime = sqlite3_column_int64(stmt, 9);
|
||||
toAdd.DeleteTime = sqlite3_column_int64(stmt, 10);
|
||||
|
||||
emails.push_back(toAdd);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return emails;
|
||||
}
|
||||
|
||||
EmailData Database::getEmail(int playerID, int index) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT
|
||||
ItemFlag, ReadFlag, SenderID, SenderFirstName,
|
||||
SenderLastName, SubjectLine, MsgBody,
|
||||
Taros, SendTime, DeleteTime
|
||||
FROM EmailData
|
||||
WHERE PlayerID = ? AND MsgIndex = ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, playerID);
|
||||
sqlite3_bind_int(stmt, 2, index);
|
||||
|
||||
EmailData result;
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
std::cout << "[WARN] Database: Email not found!" << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.PlayerId = playerID;
|
||||
result.MsgIndex = index;
|
||||
result.ItemFlag = sqlite3_column_int(stmt, 0);
|
||||
result.ReadFlag = sqlite3_column_int(stmt, 1);
|
||||
result.SenderId = sqlite3_column_int(stmt, 2);
|
||||
result.SenderFirstName = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3)));
|
||||
result.SenderLastName = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4)));
|
||||
result.SubjectLine = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 5)));
|
||||
result.MsgBody = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6)));
|
||||
result.Taros = sqlite3_column_int(stmt, 7);
|
||||
result.SendTime = sqlite3_column_int64(stmt, 8);
|
||||
result.DeleteTime = sqlite3_column_int64(stmt, 9);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
sItemBase* Database::getEmailAttachments(int playerID, int index) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
sItemBase* items = new sItemBase[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
items[i] = { 0, 0, 0, 0 };
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT Slot, ID, Type, Opt, TimeLimit
|
||||
FROM EmailItems
|
||||
WHERE PlayerID = ? AND MsgIndex = ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, playerID);
|
||||
sqlite3_bind_int(stmt, 2, index);
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int slot = sqlite3_column_int(stmt, 0) - 1;
|
||||
if (slot < 0 || slot > 3) {
|
||||
std::cout << "[WARN] Email item has invalid slot number ?!" << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
items[slot].iID = sqlite3_column_int(stmt, 1);
|
||||
items[slot].iType = sqlite3_column_int(stmt, 2);
|
||||
items[slot].iOpt = sqlite3_column_int(stmt, 3);
|
||||
items[slot].iTimeLimit = sqlite3_column_int(stmt, 4);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return items;
|
||||
}
|
||||
|
||||
void Database::updateEmailContent(EmailData* data) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT COUNT(*)
|
||||
FROM EmailItems
|
||||
WHERE PlayerID = ? AND MsgIndex = ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, data->PlayerId);
|
||||
sqlite3_bind_int(stmt, 2, data->MsgIndex);
|
||||
sqlite3_step(stmt);
|
||||
int attachmentsCount = sqlite3_column_int(stmt, 0);
|
||||
|
||||
// set attachment flag dynamically
|
||||
data->ItemFlag = (data->Taros > 0 || attachmentsCount > 0) ? 1 : 0;
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
UPDATE EmailData
|
||||
SET
|
||||
PlayerID = ?,
|
||||
MsgIndex = ?,
|
||||
ReadFlag = ?,
|
||||
ItemFlag = ?,
|
||||
SenderID = ?,
|
||||
SenderFirstName = ?,
|
||||
SenderLastName = ?,
|
||||
SubjectLine = ?,
|
||||
MsgBody = ?,
|
||||
Taros = ?,
|
||||
SendTime = ?,
|
||||
DeleteTime = ?
|
||||
WHERE PlayerID = ? AND MsgIndex = ?;
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, data->PlayerId);
|
||||
sqlite3_bind_int(stmt, 2, data->MsgIndex);
|
||||
sqlite3_bind_int(stmt, 3, data->ReadFlag);
|
||||
sqlite3_bind_int(stmt, 4, data->ItemFlag);
|
||||
sqlite3_bind_int(stmt, 5, data->SenderId);
|
||||
sqlite3_bind_text(stmt, 6, data->SenderFirstName.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 7, data->SenderLastName.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 8, data->SubjectLine.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 9, data->MsgBody.c_str(), -1, NULL);
|
||||
sqlite3_bind_int(stmt, 10, data->Taros);
|
||||
sqlite3_bind_int64(stmt, 11, data->SendTime);
|
||||
sqlite3_bind_int64(stmt, 12, data->DeleteTime);
|
||||
sqlite3_bind_int(stmt, 13, data->PlayerId);
|
||||
sqlite3_bind_int(stmt, 14, data->MsgIndex);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE)
|
||||
std::cout << "[WARN] Database: failed to update email: " << sqlite3_errmsg(db) << std::endl;
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void Database::deleteEmailAttachments(int playerID, int index, int slot) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
std::string sql(R"(
|
||||
DELETE FROM EmailItems
|
||||
WHERE PlayerID = ? AND MsgIndex = ?;
|
||||
)");
|
||||
|
||||
if (slot != -1)
|
||||
sql += " AND \"Slot\" = ? ";
|
||||
sql += ";";
|
||||
|
||||
sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, playerID);
|
||||
sqlite3_bind_int(stmt, 2, index);
|
||||
if (slot != -1)
|
||||
sqlite3_bind_int(stmt, 3, slot);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE)
|
||||
std::cout << "[WARN] Database: Failed to delete email attachments: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void Database::deleteEmails(int playerID, int64_t* indices) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
const char* sql = R"(
|
||||
DELETE FROM EmailData
|
||||
WHERE PlayerID = ? AND MsgIndex = ?;
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
sqlite3_bind_int(stmt, 1, playerID);
|
||||
sqlite3_bind_int64(stmt, 2, indices[i]);
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
std::cout << "[WARN] Database: Failed to delete an email: " << sqlite3_errmsg(db) << std::endl;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
int Database::getNextEmailIndex(int playerID) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT MsgIndex
|
||||
FROM EmailData
|
||||
WHERE PlayerID = ?
|
||||
ORDER BY MsgIndex DESC
|
||||
LIMIT 1;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, playerID);
|
||||
sqlite3_step(stmt);
|
||||
int index = sqlite3_column_int(stmt, 0);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return (index > 0 ? index + 1 : 1);
|
||||
}
|
||||
|
||||
bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments, Player *sender) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
||||
|
||||
const char* sql = R"(
|
||||
INSERT INTO EmailData
|
||||
(PlayerID, MsgIndex, ReadFlag, ItemFlag,
|
||||
SenderID, SenderFirstName, SenderLastName,
|
||||
SubjectLine, MsgBody, Taros, SendTime, DeleteTime)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, data->PlayerId);
|
||||
sqlite3_bind_int(stmt, 2, data->MsgIndex);
|
||||
sqlite3_bind_int(stmt, 3, data->ReadFlag);
|
||||
sqlite3_bind_int(stmt, 4, data->ItemFlag);
|
||||
sqlite3_bind_int(stmt, 5, data->SenderId);
|
||||
sqlite3_bind_text(stmt, 6, data->SenderFirstName.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 7, data->SenderLastName.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 8, data->SubjectLine.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 9, data->MsgBody.c_str(), -1, NULL);
|
||||
sqlite3_bind_int(stmt, 10, data->Taros);
|
||||
sqlite3_bind_int64(stmt, 11, data->SendTime);
|
||||
sqlite3_bind_int64(stmt, 12, data->DeleteTime);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
std::cout << "[WARN] Database: Failed to send email: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
INSERT INTO EmailItems
|
||||
(PlayerID, MsgIndex, Slot, ID, Type, Opt, TimeLimit)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
)";
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
// send attachments
|
||||
int slot = 1;
|
||||
for (sItemBase item : attachments) {
|
||||
sqlite3_bind_int(stmt, 1, data->PlayerId);
|
||||
sqlite3_bind_int(stmt, 2, data->MsgIndex);
|
||||
sqlite3_bind_int(stmt, 3, slot++);
|
||||
sqlite3_bind_int(stmt, 4, item.iID);
|
||||
sqlite3_bind_int(stmt, 5, item.iType);
|
||||
sqlite3_bind_int(stmt, 6, item.iOpt);
|
||||
sqlite3_bind_int(stmt, 7, item.iTimeLimit);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
std::cout << "[WARN] Database: Failed to send email: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (!_updatePlayer(sender)) {
|
||||
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
|
||||
return true;
|
||||
}
|
||||
288
src/db/init.cpp
Normal file
288
src/db/init.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
#include "db/internal.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
std::mutex dbCrit;
|
||||
sqlite3 *db;
|
||||
|
||||
/*
|
||||
* When migrating from DB version 3 to 4, we change the username column
|
||||
* to be case-insensitive. This function ensures there aren't any
|
||||
* duplicates, e.g. username and USERNAME, before doing the migration.
|
||||
* I handled this in the code itself rather than the migration file just so
|
||||
* we can have a more detailed error message than what SQLite provides.
|
||||
*/
|
||||
static void checkCaseSensitiveDupes() {
|
||||
const char* sql = "SELECT Login, COUNT(*) FROM Accounts GROUP BY LOWER(Login) HAVING COUNT(*) > 1;";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
int stat = sqlite3_step(stmt);
|
||||
|
||||
if (stat == SQLITE_DONE) {
|
||||
// no rows returned, so we're good
|
||||
sqlite3_finalize(stmt);
|
||||
return;
|
||||
} else if (stat != SQLITE_ROW) {
|
||||
std::cout << "[FATAL] Failed to check for duplicate accounts: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
std::cout << "[FATAL] Case-sensitive duplicates detected in the Login column." << std::endl;
|
||||
std::cout << "Either manually delete/rename the offending accounts, or run the pruning script:" << std::endl;
|
||||
std::cout << "https://github.com/OpenFusionProject/scripts/tree/main/db_migration/caseinsens.py" << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
static void createMetaTable() {
|
||||
std::lock_guard<std::mutex> lock(dbCrit); // XXX
|
||||
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
||||
|
||||
const char* sql = R"(
|
||||
CREATE TABLE Meta(
|
||||
Key TEXT NOT NULL UNIQUE,
|
||||
Value INTEGER NOT NULL
|
||||
);
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
exit(1);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
INSERT INTO Meta (Key, Value)
|
||||
VALUES (?, ?);
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_text(stmt, 1, "ProtocolVersion", -1, NULL);
|
||||
sqlite3_bind_int(stmt, 2, PROTOCOL_VERSION);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_bind_text(stmt, 1, "DatabaseVersion", -1, NULL);
|
||||
sqlite3_bind_int(stmt, 2, DATABASE_VERSION);
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
|
||||
std::cout << "[INFO] Created new meta table" << std::endl;
|
||||
}
|
||||
|
||||
static void checkMetaTable() {
|
||||
// first check if meta table exists
|
||||
const char* sql = R"(
|
||||
SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='Meta';
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
std::cout << "[FATAL] Failed to check meta table: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int count = sqlite3_column_int(stmt, 0);
|
||||
if (count == 0) {
|
||||
sqlite3_finalize(stmt);
|
||||
// check if there's other non-internal tables first
|
||||
sql = R"(
|
||||
SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%';
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW || sqlite3_column_int(stmt, 0) != 0) {
|
||||
sqlite3_finalize(stmt);
|
||||
std::cout << "[FATAL] Existing DB is outdated" << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// create meta table
|
||||
sqlite3_finalize(stmt);
|
||||
return createMetaTable();
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// check protocol version
|
||||
sql = R"(
|
||||
SELECT Value FROM Meta WHERE Key = 'ProtocolVersion';
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
std::cout << "[FATAL] Failed to check DB Protocol Version: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (sqlite3_column_int(stmt, 0) != PROTOCOL_VERSION) {
|
||||
sqlite3_finalize(stmt);
|
||||
std::cout << "[FATAL] DB Protocol Version doesn't match Server Build" << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
SELECT Value FROM Meta WHERE Key = 'DatabaseVersion';
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
std::cout << "[FATAL] Failed to check DB Version: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int dbVersion = sqlite3_column_int(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (dbVersion > DATABASE_VERSION) {
|
||||
std::cout << "[FATAL] Server Build is incompatible with DB Version" << std::endl;
|
||||
exit(1);
|
||||
} else if (dbVersion < DATABASE_VERSION) {
|
||||
// we're gonna migrate; back up the DB
|
||||
std::cout << "[INFO] Backing up database" << std::endl;
|
||||
// copy db file over using binary streams
|
||||
std::ifstream src(settings::DBPATH, std::ios::binary);
|
||||
std::ofstream dst(settings::DBPATH + ".old." + std::to_string(dbVersion), std::ios::binary);
|
||||
dst << src.rdbuf();
|
||||
src.close();
|
||||
dst.close();
|
||||
}
|
||||
|
||||
while (dbVersion != DATABASE_VERSION) {
|
||||
// need to run this before we do any migration logic
|
||||
if (dbVersion == 3)
|
||||
checkCaseSensitiveDupes();
|
||||
|
||||
// db migrations
|
||||
std::cout << "[INFO] Migrating Database to Version " << dbVersion + 1 << std::endl;
|
||||
|
||||
std::string path = "sql/migration" + std::to_string(dbVersion) + ".sql";
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
std::cout << "[FATAL] Failed to migrate database: Couldn't open migration file" << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
std::string sql = stream.str();
|
||||
int rc = sqlite3_exec(db, sql.c_str(), NULL, NULL, NULL);
|
||||
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cout << "[FATAL] Failed to migrate database: " << sqlite3_errmsg(db) << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
dbVersion++;
|
||||
std::cout << "[INFO] Successful Database Migration to Version " << dbVersion << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
static void createTables() {
|
||||
std::ifstream file("sql/tables.sql");
|
||||
if (!file.is_open()) {
|
||||
std::cout << "[FATAL] Failed to open database scheme" << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
std::string read = stream.str();
|
||||
const char* sql = read.c_str();
|
||||
|
||||
char* errMsg = 0;
|
||||
int rc = sqlite3_exec(db, sql, NULL, NULL, &errMsg);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cout << "[FATAL] Database failed to create tables: " << errMsg << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
static int getTableSize(std::string tableName) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit); // XXX
|
||||
|
||||
const char* sql = "SELECT COUNT(*) FROM ?";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_text(stmt, 1, tableName.c_str(), -1, NULL);
|
||||
sqlite3_step(stmt);
|
||||
int result = sqlite3_column_int(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
void Database::init() {
|
||||
std::cout << "[INFO] Built with libsqlite " SQLITE_VERSION << std::endl;
|
||||
|
||||
if (sqlite3_libversion_number() != SQLITE_VERSION_NUMBER)
|
||||
std::cout << "[INFO] Using libsqlite " << std::string(sqlite3_libversion()) << std::endl;
|
||||
|
||||
if (sqlite3_libversion_number() < MIN_SUPPORTED_SQLITE_NUMBER) {
|
||||
std::cerr << "[FATAL] Runtime sqlite version too old. Minimum compatible version: " MIN_SUPPORTED_SQLITE << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void Database::open() {
|
||||
|
||||
// XXX: move locks here
|
||||
int rc = sqlite3_open(settings::DBPATH.c_str(), &db);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cout << "[FATAL] Cannot open database: " << sqlite3_errmsg(db) << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// foreign keys in sqlite are off by default; enable them
|
||||
sqlite3_exec(db, "PRAGMA foreign_keys=ON;", NULL, NULL, NULL);
|
||||
|
||||
// just in case a DB operation collides with an external manual modification
|
||||
sqlite3_busy_timeout(db, 2000);
|
||||
|
||||
checkMetaTable();
|
||||
createTables();
|
||||
|
||||
std::cout << "[INFO] Database in operation ";
|
||||
int accounts = getTableSize("Accounts");
|
||||
int players = getTableSize("Players");
|
||||
std::string message = "";
|
||||
if (accounts > 0) {
|
||||
message += ": Found " + std::to_string(accounts) + " Account";
|
||||
if (accounts > 1)
|
||||
message += "s";
|
||||
}
|
||||
if (players > 0) {
|
||||
message += " and " + std::to_string(players) + " Player Character";
|
||||
if (players > 1)
|
||||
message += "s";
|
||||
}
|
||||
std::cout << message << std::endl;
|
||||
}
|
||||
|
||||
void Database::close() {
|
||||
sqlite3_close(db);
|
||||
}
|
||||
18
src/db/internal.hpp
Normal file
18
src/db/internal.hpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "db/Database.hpp"
|
||||
#include <sqlite3.h>
|
||||
|
||||
#define MIN_SUPPORTED_SQLITE_NUMBER 3033000
|
||||
#define MIN_SUPPORTED_SQLITE "3.33.0"
|
||||
// we can't use this in #error, since it doesn't expand macros
|
||||
|
||||
// Compile-time libsqlite version check
|
||||
#if SQLITE_VERSION_NUMBER < MIN_SUPPORTED_SQLITE_NUMBER
|
||||
#error libsqlite version too old. Minimum compatible version: 3.33.0
|
||||
#endif
|
||||
|
||||
extern std::mutex dbCrit;
|
||||
extern sqlite3 *db;
|
||||
|
||||
using namespace Database;
|
||||
559
src/db/login.cpp
Normal file
559
src/db/login.cpp
Normal file
@@ -0,0 +1,559 @@
|
||||
#include "db/internal.hpp"
|
||||
|
||||
#include "bcrypt/BCrypt.hpp"
|
||||
|
||||
void Database::findAccount(Account* account, std::string login) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT AccountID, Password, Selected, BannedUntil, BanReason
|
||||
FROM Accounts
|
||||
WHERE Login = ?
|
||||
LIMIT 1;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_text(stmt, 1, login.c_str(), -1, NULL);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
account->AccountID = sqlite3_column_int(stmt, 0);
|
||||
account->Password = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1)));
|
||||
account->Selected = sqlite3_column_int(stmt, 2);
|
||||
account->BannedUntil = sqlite3_column_int64(stmt, 3);
|
||||
account->BanReason = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
int Database::addAccount(std::string login, std::string password) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
INSERT INTO Accounts (Login, Password, AccountLevel)
|
||||
VALUES (?, ?, ?);
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_text(stmt, 1, login.c_str(), -1, NULL);
|
||||
std::string hashedPassword = BCrypt::generateHash(password);
|
||||
sqlite3_bind_text(stmt, 2, hashedPassword.c_str(), -1, NULL);
|
||||
sqlite3_bind_int(stmt, 3, settings::ACCLEVEL);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
std::cout << "[WARN] Database: failed to add new account" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return sqlite3_last_insert_rowid(db);
|
||||
}
|
||||
|
||||
void Database::updateSelected(int accountId, int slot) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
if (slot < 1 || slot > 4) {
|
||||
std::cout << "[WARN] Invalid slot number passed to updateSelected()! " << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* sql = R"(
|
||||
UPDATE Accounts SET
|
||||
Selected = ?,
|
||||
LastLogin = (strftime('%s', 'now'))
|
||||
WHERE AccountID = ?;
|
||||
)";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, slot);
|
||||
sqlite3_bind_int(stmt, 2, accountId);
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE)
|
||||
std::cout << "[WARN] Database fail on updateSelected(): " << sqlite3_errmsg(db) << std::endl;
|
||||
}
|
||||
|
||||
void Database::updateSelectedByPlayerId(int accountId, int32_t playerId) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
UPDATE Accounts SET
|
||||
Selected = p.Slot,
|
||||
LastLogin = (strftime('%s', 'now'))
|
||||
FROM (SELECT Slot From Players WHERE PlayerId = ?) AS p
|
||||
WHERE AccountID = ?;
|
||||
)";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, playerId);
|
||||
sqlite3_bind_int(stmt, 2, accountId);
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE)
|
||||
std::cout << "[WARN] Database fail on updateSelectedByPlayerId(): " << sqlite3_errmsg(db) << std::endl;
|
||||
}
|
||||
|
||||
bool Database::validateCharacter(int characterID, int userID) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
// query whatever
|
||||
const char* sql = R"(
|
||||
SELECT PlayerID
|
||||
FROM Players
|
||||
WHERE PlayerID = ? AND AccountID = ?
|
||||
LIMIT 1;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, characterID);
|
||||
sqlite3_bind_int(stmt, 2, userID);
|
||||
int rc = sqlite3_step(stmt);
|
||||
// if we got a row back, the character is valid
|
||||
bool result = (rc == SQLITE_ROW);
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Database::isNameFree(std::string firstName, std::string lastName) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT COUNT(*)
|
||||
FROM Players
|
||||
WHERE FirstName = ? AND LastName = ?
|
||||
LIMIT 1;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_text(stmt, 1, firstName.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 2, lastName.c_str(), -1, NULL);
|
||||
int rc = sqlite3_step(stmt);
|
||||
|
||||
bool result = (rc == SQLITE_ROW && sqlite3_column_int(stmt, 0) == 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Database::isSlotFree(int accountId, int slotNum) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
if (slotNum < 1 || slotNum > 4) {
|
||||
std::cout << "[WARN] Invalid slot number passed to isSlotFree()! " << slotNum << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT COUNT(*)
|
||||
FROM Players
|
||||
WHERE AccountID = ? AND Slot = ?
|
||||
LIMIT 1;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, accountId);
|
||||
sqlite3_bind_int(stmt, 2, slotNum);
|
||||
int rc = sqlite3_step(stmt);
|
||||
|
||||
bool result = (rc == SQLITE_ROW && sqlite3_column_int(stmt, 0) == 0);
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
int Database::createCharacter(sP_CL2LS_REQ_SAVE_CHAR_NAME* save, int AccountID) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
||||
|
||||
const char* sql = R"(
|
||||
INSERT INTO Players
|
||||
(AccountID, Slot, FirstName, LastName,
|
||||
XCoordinate, YCoordinate, ZCoordinate, Angle,
|
||||
HP, NameCheck, Quests, SkywayLocationFlag, FirstUseFlag)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
std::string firstName = AUTOU16TOU8(save->szFirstName);
|
||||
std::string lastName = AUTOU16TOU8(save->szLastName);
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, AccountID);
|
||||
sqlite3_bind_int(stmt, 2, save->iSlotNum);
|
||||
sqlite3_bind_text(stmt, 3, firstName.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 4, lastName.c_str(), -1, NULL);
|
||||
sqlite3_bind_int(stmt, 5, settings::SPAWN_X);
|
||||
sqlite3_bind_int(stmt, 6, settings::SPAWN_Y);
|
||||
sqlite3_bind_int(stmt, 7, settings::SPAWN_Z);
|
||||
sqlite3_bind_int(stmt, 8, settings::SPAWN_ANGLE);
|
||||
sqlite3_bind_int(stmt, 9, PC_MAXHEALTH(1));
|
||||
|
||||
// if FNCode isn't 0, it's a wheel name
|
||||
int nameCheck = (settings::APPROVEALLNAMES || save->iFNCode) ? 1 : 0;
|
||||
sqlite3_bind_int(stmt, 10, nameCheck);
|
||||
|
||||
// blobs
|
||||
unsigned char blobBuffer[sizeof(Player::aQuestFlag)] = { 0 };
|
||||
sqlite3_bind_blob(stmt, 11, blobBuffer, sizeof(Player::aQuestFlag), NULL);
|
||||
sqlite3_bind_blob(stmt, 12, blobBuffer, sizeof(Player::aSkywayLocationFlag), NULL);
|
||||
sqlite3_bind_blob(stmt, 13, blobBuffer, sizeof(Player::iFirstUseFlag), NULL);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int playerId = sqlite3_last_insert_rowid(db);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
INSERT INTO Appearances (PlayerID)
|
||||
VALUES (?);
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, playerId);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
|
||||
return playerId;
|
||||
}
|
||||
|
||||
bool Database::finishCharacter(sP_CL2LS_REQ_CHAR_CREATE* character, int accountId) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
||||
|
||||
const char* sql = R"(
|
||||
UPDATE Players
|
||||
SET AppearanceFlag = 1
|
||||
WHERE PlayerID = ? AND AccountID = ? AND AppearanceFlag = 0;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, character->PCStyle.iPC_UID);
|
||||
sqlite3_bind_int(stmt, 2, accountId);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
UPDATE Appearances
|
||||
SET
|
||||
Body = ?,
|
||||
EyeColor = ?,
|
||||
FaceStyle = ?,
|
||||
Gender = ?,
|
||||
HairColor = ?,
|
||||
HairStyle = ?,
|
||||
Height = ?,
|
||||
SkinColor = ?
|
||||
WHERE PlayerID = ?;
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
sqlite3_bind_int(stmt, 1, character->PCStyle.iBody);
|
||||
sqlite3_bind_int(stmt, 2, character->PCStyle.iEyeColor);
|
||||
sqlite3_bind_int(stmt, 3, character->PCStyle.iFaceStyle);
|
||||
sqlite3_bind_int(stmt, 4, character->PCStyle.iGender);
|
||||
sqlite3_bind_int(stmt, 5, character->PCStyle.iHairColor);
|
||||
sqlite3_bind_int(stmt, 6, character->PCStyle.iHairStyle);
|
||||
sqlite3_bind_int(stmt, 7, character->PCStyle.iHeight);
|
||||
sqlite3_bind_int(stmt, 8, character->PCStyle.iSkinColor);
|
||||
sqlite3_bind_int(stmt, 9, character->PCStyle.iPC_UID);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
INSERT INTO Inventory (PlayerID, Slot, ID, Type, Opt)
|
||||
VALUES (?, ?, ?, ?, 1);
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
|
||||
|
||||
int items[3] = { character->sOn_Item.iEquipUBID, character->sOn_Item.iEquipLBID, character->sOn_Item.iEquipFootID };
|
||||
for (int i = 0; i < 3; i++) {
|
||||
sqlite3_bind_int(stmt, 1, character->PCStyle.iPC_UID);
|
||||
sqlite3_bind_int(stmt, 2, i+1);
|
||||
sqlite3_bind_int(stmt, 3, items[i]);
|
||||
sqlite3_bind_int(stmt, 4, i+1);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return false;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Database::finishTutorial(int playerID, int accountID) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
||||
|
||||
const char* sql = R"(
|
||||
UPDATE Players SET
|
||||
TutorialFlag = 1,
|
||||
Nano1 = ?,
|
||||
Quests = ?
|
||||
WHERE PlayerID = ? AND AccountID = ? AND TutorialFlag = 0;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
unsigned char questBuffer[128] = { 0 };
|
||||
|
||||
#ifndef ACADEMY
|
||||
// save missions nr 1 & 2; equip Buttercup
|
||||
questBuffer[0] = 3;
|
||||
sqlite3_bind_int(stmt, 1, 1);
|
||||
#else
|
||||
// no, none of that
|
||||
sqlite3_bind_int(stmt, 1, 0);
|
||||
#endif
|
||||
|
||||
sqlite3_bind_blob(stmt, 2, questBuffer, sizeof(questBuffer), NULL);
|
||||
sqlite3_bind_int(stmt, 3, playerID);
|
||||
sqlite3_bind_int(stmt, 4, accountID);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
#ifndef ACADEMY
|
||||
// Lightning Gun
|
||||
sql = R"(
|
||||
INSERT INTO Inventory
|
||||
(PlayerID, Slot, ID, Type, Opt)
|
||||
VALUES (?, 0, 328, 0, 1);
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
sqlite3_bind_int(stmt, 1, playerID);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// Nano Buttercup
|
||||
sql = R"(
|
||||
INSERT INTO Nanos
|
||||
(PlayerID, ID, Skill)
|
||||
VALUES (?, 1, 1);
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
sqlite3_bind_int(stmt, 1, playerID);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE) {
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
|
||||
return true;
|
||||
}
|
||||
|
||||
int Database::deleteCharacter(int characterID, int userID) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT Slot
|
||||
FROM Players
|
||||
WHERE AccountID = ? AND PlayerID = ?
|
||||
LIMIT 1;
|
||||
)";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, userID);
|
||||
sqlite3_bind_int(stmt, 2, characterID);
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
return 0;
|
||||
}
|
||||
int slot = sqlite3_column_int(stmt, 0);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
DELETE FROM Players
|
||||
WHERE AccountID = ? AND PlayerID = ?;
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
|
||||
sqlite3_bind_int(stmt, 1, userID);
|
||||
sqlite3_bind_int(stmt, 2, characterID);
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE)
|
||||
return 0;
|
||||
|
||||
return slot;
|
||||
}
|
||||
|
||||
void Database::getCharInfo(std::vector <sP_LS2CL_REP_CHAR_INFO>* result, int userID) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT
|
||||
p.PlayerID, p.Slot, p.FirstName, p.LastName, p.Level, p.AppearanceFlag, p.TutorialFlag, p.PayZoneFlag,
|
||||
p.XCoordinate, p.YCoordinate, p.ZCoordinate, p.NameCheck,
|
||||
a.Body, a.EyeColor, a.FaceStyle, a.Gender, a.HairColor, a.HairStyle, a.Height, a.SkinColor
|
||||
FROM Players as p
|
||||
INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
|
||||
WHERE p.AccountID = ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, userID);
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
sP_LS2CL_REP_CHAR_INFO toAdd = {};
|
||||
toAdd.sPC_Style.iPC_UID = sqlite3_column_int(stmt, 0);
|
||||
toAdd.iSlot = sqlite3_column_int(stmt, 1);
|
||||
|
||||
// parsing const unsigned char* to char16_t
|
||||
std::string placeHolder = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2)));
|
||||
U8toU16(placeHolder, toAdd.sPC_Style.szFirstName, sizeof(toAdd.sPC_Style.szFirstName));
|
||||
placeHolder = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3)));
|
||||
U8toU16(placeHolder, toAdd.sPC_Style.szLastName, sizeof(toAdd.sPC_Style.szLastName));
|
||||
|
||||
toAdd.iLevel = sqlite3_column_int(stmt, 4);
|
||||
toAdd.sPC_Style2.iAppearanceFlag = sqlite3_column_int(stmt, 5);
|
||||
toAdd.sPC_Style2.iTutorialFlag = sqlite3_column_int(stmt, 6);
|
||||
toAdd.sPC_Style2.iPayzoneFlag = sqlite3_column_int(stmt, 7);
|
||||
toAdd.iX = sqlite3_column_int(stmt, 8);
|
||||
toAdd.iY = sqlite3_column_int(stmt, 9);
|
||||
toAdd.iZ = sqlite3_column_int(stmt, 10);
|
||||
toAdd.sPC_Style.iNameCheck = sqlite3_column_int(stmt, 11);
|
||||
toAdd.sPC_Style.iBody = sqlite3_column_int(stmt, 12);
|
||||
toAdd.sPC_Style.iEyeColor = sqlite3_column_int(stmt, 13);
|
||||
toAdd.sPC_Style.iFaceStyle = sqlite3_column_int(stmt, 14);
|
||||
toAdd.sPC_Style.iGender = sqlite3_column_int(stmt, 15);
|
||||
toAdd.sPC_Style.iHairColor = sqlite3_column_int(stmt, 16);
|
||||
toAdd.sPC_Style.iHairStyle = sqlite3_column_int(stmt, 17);
|
||||
toAdd.sPC_Style.iHeight = sqlite3_column_int(stmt, 18);
|
||||
toAdd.sPC_Style.iSkinColor = sqlite3_column_int(stmt, 19);
|
||||
|
||||
// request aEquip
|
||||
const char* sql2 = R"(
|
||||
SELECT Slot, Type, ID, Opt, TimeLimit
|
||||
FROM Inventory
|
||||
WHERE PlayerID = ? AND Slot < ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt2;
|
||||
|
||||
sqlite3_prepare_v2(db, sql2, -1, &stmt2, NULL);
|
||||
sqlite3_bind_int(stmt2, 1, toAdd.sPC_Style.iPC_UID);
|
||||
sqlite3_bind_int(stmt2, 2, AEQUIP_COUNT);
|
||||
|
||||
while (sqlite3_step(stmt2) == SQLITE_ROW) {
|
||||
sItemBase* item = &toAdd.aEquip[sqlite3_column_int(stmt2, 0)];
|
||||
item->iType = sqlite3_column_int(stmt2, 1);
|
||||
item->iID = sqlite3_column_int(stmt2, 2);
|
||||
item->iOpt = sqlite3_column_int(stmt2, 3);
|
||||
item->iTimeLimit = sqlite3_column_int(stmt2, 4);
|
||||
}
|
||||
sqlite3_finalize(stmt2);
|
||||
|
||||
result->push_back(toAdd);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// NOTE: This is currently never called.
|
||||
void Database::evaluateCustomName(int characterID, CustomName decision) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
UPDATE Players
|
||||
SET NameCheck = ?
|
||||
WHERE PlayerID = ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, int(decision));
|
||||
sqlite3_bind_int(stmt, 2, characterID);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE)
|
||||
std::cout << "[WARN] Database: Failed to update nameCheck: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
bool Database::changeName(sP_CL2LS_REQ_CHANGE_CHAR_NAME* save, int accountId) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
UPDATE Players
|
||||
SET
|
||||
FirstName = ?,
|
||||
LastName = ?,
|
||||
NameCheck = ?
|
||||
WHERE PlayerID = ? AND AccountID = ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
std::string firstName = AUTOU16TOU8(save->szFirstName);
|
||||
std::string lastName = AUTOU16TOU8(save->szLastName);
|
||||
|
||||
sqlite3_bind_text(stmt, 1, firstName.c_str(), -1, NULL);
|
||||
sqlite3_bind_text(stmt, 2, lastName.c_str(), -1, NULL);
|
||||
// if FNCode isn't 0, it's a wheel name
|
||||
int nameCheck = (settings::APPROVEALLNAMES || save->iFNCode) ? 1 : 0;
|
||||
sqlite3_bind_int(stmt, 3, nameCheck);
|
||||
sqlite3_bind_int(stmt, 4, save->iPCUID);
|
||||
sqlite3_bind_int(stmt, 5, accountId);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return rc == SQLITE_DONE;
|
||||
}
|
||||
560
src/db/player.cpp
Normal file
560
src/db/player.cpp
Normal file
@@ -0,0 +1,560 @@
|
||||
#include "db/internal.hpp"
|
||||
|
||||
// Loading and saving players to/from the DB
|
||||
|
||||
static void removeExpiredVehicles(Player* player) {
|
||||
int32_t currentTime = getTimestamp();
|
||||
|
||||
// if there are expired vehicles in bank just remove them silently
|
||||
for (int i = 0; i < ABANK_COUNT; i++) {
|
||||
if (player->Bank[i].iType == 10 && player->Bank[i].iTimeLimit < currentTime && player->Bank[i].iTimeLimit != 0) {
|
||||
memset(&player->Bank[i], 0, sizeof(sItemBase));
|
||||
}
|
||||
}
|
||||
|
||||
// we want to leave only 1 expired vehicle on player to delete it with the client packet
|
||||
std::vector<sItemBase*> toRemove;
|
||||
|
||||
// equipped vehicle
|
||||
if (player->Equip[8].iOpt > 0 && player->Equip[8].iTimeLimit < currentTime && player->Equip[8].iTimeLimit != 0) {
|
||||
toRemove.push_back(&player->Equip[8]);
|
||||
player->toRemoveVehicle.eIL = 0;
|
||||
player->toRemoveVehicle.iSlotNum = 8;
|
||||
}
|
||||
// inventory
|
||||
for (int i = 0; i < AINVEN_COUNT; i++) {
|
||||
if (player->Inven[i].iType == 10 && player->Inven[i].iTimeLimit < currentTime && player->Inven[i].iTimeLimit != 0) {
|
||||
toRemove.push_back(&player->Inven[i]);
|
||||
player->toRemoveVehicle.eIL = 1;
|
||||
player->toRemoveVehicle.iSlotNum = i;
|
||||
}
|
||||
}
|
||||
|
||||
// delete all but one vehicles, leave last one for ceremonial deletion
|
||||
for (int i = 0; i < (int)toRemove.size()-1; i++) {
|
||||
memset(toRemove[i], 0, sizeof(sItemBase));
|
||||
}
|
||||
}
|
||||
|
||||
void Database::getPlayer(Player* plr, int id) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
const char* sql = R"(
|
||||
SELECT
|
||||
p.AccountID, p.Slot, p.FirstName, p.LastName,
|
||||
p.Level, p.Nano1, p.Nano2, p.Nano3,
|
||||
p.AppearanceFlag, p.TutorialFlag, p.PayZoneFlag,
|
||||
p.XCoordinate, p.YCoordinate, p.ZCoordinate, p.NameCheck,
|
||||
p.Angle, p.HP, acc.AccountLevel, p.FusionMatter, p.Taros, p.Quests,
|
||||
p.BatteryW, p.BatteryN, p.Mentor, p.WarpLocationFlag,
|
||||
p.SkywayLocationFlag, p.CurrentMissionID, p.FirstUseFlag,
|
||||
a.Body, a.EyeColor, a.FaceStyle, a.Gender, a.HairColor, a.HairStyle, a.Height, a.SkinColor
|
||||
FROM Players as p
|
||||
INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
|
||||
INNER JOIN Accounts as acc ON p.AccountID = acc.AccountID
|
||||
WHERE p.PlayerID = ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, id);
|
||||
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
std::cout << "[WARN] Database: Failed to load character [" << id << "]: " << sqlite3_errmsg(db) << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
plr->iID = id;
|
||||
plr->PCStyle.iPC_UID = id;
|
||||
|
||||
plr->accountId = sqlite3_column_int(stmt, 0);
|
||||
plr->slot = sqlite3_column_int(stmt, 1);
|
||||
|
||||
// parsing const unsigned char* to char16_t
|
||||
std::string placeHolder = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2)));
|
||||
U8toU16(placeHolder, plr->PCStyle.szFirstName, sizeof(plr->PCStyle.szFirstName));
|
||||
placeHolder = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3)));
|
||||
U8toU16(placeHolder, plr->PCStyle.szLastName, sizeof(plr->PCStyle.szLastName));
|
||||
|
||||
plr->level = sqlite3_column_int(stmt, 4);
|
||||
plr->equippedNanos[0] = sqlite3_column_int(stmt, 5);
|
||||
plr->equippedNanos[1] = sqlite3_column_int(stmt, 6);
|
||||
plr->equippedNanos[2] = sqlite3_column_int(stmt, 7);
|
||||
|
||||
plr->PCStyle2.iAppearanceFlag = sqlite3_column_int(stmt, 8);
|
||||
plr->PCStyle2.iTutorialFlag = sqlite3_column_int(stmt, 9);
|
||||
plr->PCStyle2.iPayzoneFlag = sqlite3_column_int(stmt, 10);
|
||||
|
||||
plr->x = sqlite3_column_int(stmt, 11);
|
||||
plr->y = sqlite3_column_int(stmt, 12);
|
||||
plr->z = sqlite3_column_int(stmt, 13);
|
||||
plr->PCStyle.iNameCheck = sqlite3_column_int(stmt, 14);
|
||||
|
||||
plr->angle = sqlite3_column_int(stmt, 15);
|
||||
plr->HP = sqlite3_column_int(stmt, 16);
|
||||
plr->accountLevel = sqlite3_column_int(stmt, 17);
|
||||
plr->fusionmatter = sqlite3_column_int(stmt, 18);
|
||||
plr->money = sqlite3_column_int(stmt, 19);
|
||||
|
||||
memcpy(plr->aQuestFlag, sqlite3_column_blob(stmt, 20), sizeof(plr->aQuestFlag));
|
||||
|
||||
plr->batteryW = sqlite3_column_int(stmt, 21);
|
||||
plr->batteryN = sqlite3_column_int(stmt, 22);
|
||||
plr->mentor = sqlite3_column_int(stmt, 23);
|
||||
plr->iWarpLocationFlag = sqlite3_column_int(stmt, 24);
|
||||
|
||||
memcpy(plr->aSkywayLocationFlag, sqlite3_column_blob(stmt, 25), sizeof(plr->aSkywayLocationFlag));
|
||||
|
||||
plr->CurrentMissionID = sqlite3_column_int(stmt, 26);
|
||||
|
||||
memcpy(plr->iFirstUseFlag, sqlite3_column_blob(stmt, 27), sizeof(plr->iFirstUseFlag));
|
||||
|
||||
plr->PCStyle.iBody = sqlite3_column_int(stmt, 28);
|
||||
plr->PCStyle.iEyeColor = sqlite3_column_int(stmt, 29);
|
||||
plr->PCStyle.iFaceStyle = sqlite3_column_int(stmt, 30);
|
||||
plr->PCStyle.iGender = sqlite3_column_int(stmt, 31);
|
||||
plr->PCStyle.iHairColor = sqlite3_column_int(stmt, 32);
|
||||
plr->PCStyle.iHairStyle = sqlite3_column_int(stmt, 33);
|
||||
plr->PCStyle.iHeight = sqlite3_column_int(stmt, 34);
|
||||
plr->PCStyle.iSkinColor = sqlite3_column_int(stmt, 35);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// get inventory
|
||||
sql = R"(
|
||||
SELECT Slot, Type, ID, Opt, TimeLimit
|
||||
FROM Inventory
|
||||
WHERE PlayerID = ?;
|
||||
)";
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
sqlite3_bind_int(stmt, 1, id);
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int slot = sqlite3_column_int(stmt, 0);
|
||||
|
||||
// for extra safety
|
||||
if (slot < 0 || slot > AEQUIP_COUNT + AINVEN_COUNT + ABANK_COUNT) {
|
||||
std::cout << "[WARN] Database: Invalid item slot in db?! " << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
sItemBase* item;
|
||||
if (slot < AEQUIP_COUNT) {
|
||||
// equipment
|
||||
item = &plr->Equip[slot];
|
||||
} else if (slot < (AEQUIP_COUNT + AINVEN_COUNT)) {
|
||||
// inventory
|
||||
item = &plr->Inven[slot - AEQUIP_COUNT];
|
||||
} else {
|
||||
// bank
|
||||
item = &plr->Bank[slot - AEQUIP_COUNT - AINVEN_COUNT];
|
||||
}
|
||||
|
||||
item->iType = sqlite3_column_int(stmt, 1);
|
||||
item->iID = sqlite3_column_int(stmt, 2);
|
||||
item->iOpt = sqlite3_column_int(stmt, 3);
|
||||
item->iTimeLimit = sqlite3_column_int(stmt, 4);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
removeExpiredVehicles(plr);
|
||||
|
||||
// get quest inventory
|
||||
sql = R"(
|
||||
SELECT Slot, ID, Opt
|
||||
FROM QuestItems
|
||||
WHERE PlayerID = ?;
|
||||
)";
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
sqlite3_bind_int(stmt, 1, id);
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int slot = sqlite3_column_int(stmt, 0);
|
||||
|
||||
// for extra safety
|
||||
if (slot < 0)
|
||||
continue;
|
||||
|
||||
sItemBase* item = &plr->QInven[slot];
|
||||
item->iType = 8;
|
||||
item->iID = sqlite3_column_int(stmt, 1);
|
||||
item->iOpt = sqlite3_column_int(stmt, 2);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// get nanos
|
||||
sql = R"(
|
||||
SELECT ID, Skill, Stamina
|
||||
FROM Nanos
|
||||
WHERE PlayerID = ?;
|
||||
)";
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, id);
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int id = sqlite3_column_int(stmt, 0);
|
||||
|
||||
// for extra safety
|
||||
if (id < 0 || id > NANO_COUNT)
|
||||
continue;
|
||||
|
||||
sNano* nano = &plr->Nanos[id];
|
||||
nano->iID = id;
|
||||
nano->iSkillID = sqlite3_column_int(stmt, 1);
|
||||
nano->iStamina = sqlite3_column_int(stmt, 2);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// get active quests
|
||||
sql = R"(
|
||||
SELECT
|
||||
TaskID,
|
||||
RemainingNPCCount1,
|
||||
RemainingNPCCount2,
|
||||
RemainingNPCCount3
|
||||
FROM RunningQuests
|
||||
WHERE PlayerID = ?;
|
||||
)";
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, id);
|
||||
|
||||
std::set<int> tasksSet; // used to prevent duplicate tasks from loading in
|
||||
for (int i = 0; sqlite3_step(stmt) == SQLITE_ROW && i < ACTIVE_MISSION_COUNT; i++) {
|
||||
|
||||
int taskID = sqlite3_column_int(stmt, 0);
|
||||
if (tasksSet.find(taskID) != tasksSet.end())
|
||||
continue;
|
||||
|
||||
plr->tasks[i] = taskID;
|
||||
tasksSet.insert(taskID);
|
||||
plr->RemainingNPCCount[i][0] = sqlite3_column_int(stmt, 1);
|
||||
plr->RemainingNPCCount[i][1] = sqlite3_column_int(stmt, 2);
|
||||
plr->RemainingNPCCount[i][2] = sqlite3_column_int(stmt, 3);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// get buddies
|
||||
sql = R"(
|
||||
SELECT PlayerAID, PlayerBID
|
||||
FROM Buddyships
|
||||
WHERE PlayerAID = ? OR PlayerBID = ?;
|
||||
)";
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, id);
|
||||
sqlite3_bind_int(stmt, 2, id);
|
||||
|
||||
int i = 0;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW && i < 50) {
|
||||
int PlayerAId = sqlite3_column_int(stmt, 0);
|
||||
int PlayerBId = sqlite3_column_int(stmt, 1);
|
||||
|
||||
plr->buddyIDs[i] = id == PlayerAId ? PlayerBId : PlayerAId;
|
||||
plr->isBuddyBlocked[i] = false;
|
||||
i++;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// get blocked players
|
||||
sql = R"(
|
||||
SELECT BlockedPlayerID FROM Blocks
|
||||
WHERE PlayerID = ?;
|
||||
)";
|
||||
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, id);
|
||||
|
||||
// i retains its value from after the loop over Buddyships
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW && i < 50) {
|
||||
plr->buddyIDs[i] = sqlite3_column_int(stmt, 0);
|
||||
plr->isBuddyBlocked[i] = true;
|
||||
i++;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
/*
|
||||
* Low-level function to save a player to DB.
|
||||
* Must be run in a SQL transaction and with dbCrit locked.
|
||||
* The caller manages the transacstion, so if this function returns false,
|
||||
* the caller must roll it back.
|
||||
*/
|
||||
bool Database::_updatePlayer(Player *player) {
|
||||
const char* sql = R"(
|
||||
UPDATE Players
|
||||
SET
|
||||
Level = ? , Nano1 = ?, Nano2 = ?, Nano3 = ?,
|
||||
XCoordinate = ?, YCoordinate = ?, ZCoordinate = ?,
|
||||
Angle = ?, HP = ?, FusionMatter = ?, Taros = ?, Quests = ?,
|
||||
BatteryW = ?, BatteryN = ?, WarplocationFlag = ?,
|
||||
SkywayLocationFlag = ?, CurrentMissionID = ?,
|
||||
PayZoneFlag = ?, FirstUseFlag = ?, Mentor = ?
|
||||
WHERE PlayerID = ?;
|
||||
)";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, player->level);
|
||||
sqlite3_bind_int(stmt, 2, player->equippedNanos[0]);
|
||||
sqlite3_bind_int(stmt, 3, player->equippedNanos[1]);
|
||||
sqlite3_bind_int(stmt, 4, player->equippedNanos[2]);
|
||||
|
||||
if (player->instanceID == 0 && !player->onMonkey) {
|
||||
sqlite3_bind_int(stmt, 5, player->x);
|
||||
sqlite3_bind_int(stmt, 6, player->y);
|
||||
sqlite3_bind_int(stmt, 7, player->z);
|
||||
sqlite3_bind_int(stmt, 8, player->angle);
|
||||
}
|
||||
else {
|
||||
sqlite3_bind_int(stmt, 5, player->lastX);
|
||||
sqlite3_bind_int(stmt, 6, player->lastY);
|
||||
sqlite3_bind_int(stmt, 7, player->lastZ);
|
||||
sqlite3_bind_int(stmt, 8, player->lastAngle);
|
||||
}
|
||||
|
||||
sqlite3_bind_int(stmt, 9, player->HP);
|
||||
sqlite3_bind_int(stmt, 10, player->fusionmatter);
|
||||
sqlite3_bind_int(stmt, 11, player->money);
|
||||
sqlite3_bind_blob(stmt, 12, player->aQuestFlag, sizeof(player->aQuestFlag), NULL);
|
||||
sqlite3_bind_int(stmt, 13, player->batteryW);
|
||||
sqlite3_bind_int(stmt, 14, player->batteryN);
|
||||
sqlite3_bind_int(stmt, 15, player->iWarpLocationFlag);
|
||||
sqlite3_bind_blob(stmt, 16, player->aSkywayLocationFlag, sizeof(player->aSkywayLocationFlag), NULL);
|
||||
sqlite3_bind_int(stmt, 17, player->CurrentMissionID);
|
||||
sqlite3_bind_int(stmt, 18, player->PCStyle2.iPayzoneFlag);
|
||||
sqlite3_bind_blob(stmt, 19, player->iFirstUseFlag, sizeof(player->iFirstUseFlag), NULL);
|
||||
sqlite3_bind_int(stmt, 20, player->mentor);
|
||||
sqlite3_bind_int(stmt, 21, player->iID);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// update inventory
|
||||
sql = R"(
|
||||
DELETE FROM Inventory WHERE PlayerID = ?;
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, player->iID);
|
||||
int rc = sqlite3_step(stmt);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
INSERT INTO Inventory
|
||||
(PlayerID, Slot, Type, Opt, ID, Timelimit)
|
||||
VALUES (?, ?, ?, ?, ?, ?);
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
for (int i = 0; i < AEQUIP_COUNT; i++) {
|
||||
if (player->Equip[i].iID == 0)
|
||||
continue;
|
||||
|
||||
sqlite3_bind_int(stmt, 1, player->iID);
|
||||
sqlite3_bind_int(stmt, 2, i);
|
||||
sqlite3_bind_int(stmt, 3, player->Equip[i].iType);
|
||||
sqlite3_bind_int(stmt, 4, player->Equip[i].iOpt);
|
||||
sqlite3_bind_int(stmt, 5, player->Equip[i].iID);
|
||||
sqlite3_bind_int(stmt, 6, player->Equip[i].iTimeLimit);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
|
||||
for (int i = 0; i < AINVEN_COUNT; i++) {
|
||||
if (player->Inven[i].iID == 0)
|
||||
continue;
|
||||
|
||||
sqlite3_bind_int(stmt, 1, player->iID);
|
||||
sqlite3_bind_int(stmt, 2, i + AEQUIP_COUNT);
|
||||
sqlite3_bind_int(stmt, 3, player->Inven[i].iType);
|
||||
sqlite3_bind_int(stmt, 4, player->Inven[i].iOpt);
|
||||
sqlite3_bind_int(stmt, 5, player->Inven[i].iID);
|
||||
sqlite3_bind_int(stmt, 6, player->Inven[i].iTimeLimit);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
|
||||
for (int i = 0; i < ABANK_COUNT; i++) {
|
||||
if (player->Bank[i].iID == 0)
|
||||
continue;
|
||||
|
||||
sqlite3_bind_int(stmt, 1, player->iID);
|
||||
sqlite3_bind_int(stmt, 2, i + AEQUIP_COUNT + AINVEN_COUNT);
|
||||
sqlite3_bind_int(stmt, 3, player->Bank[i].iType);
|
||||
sqlite3_bind_int(stmt, 4, player->Bank[i].iOpt);
|
||||
sqlite3_bind_int(stmt, 5, player->Bank[i].iID);
|
||||
sqlite3_bind_int(stmt, 6, player->Bank[i].iTimeLimit);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// Update Quest Inventory
|
||||
sql = R"(
|
||||
DELETE FROM QuestItems WHERE PlayerID = ?;
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, player->iID);
|
||||
sqlite3_step(stmt);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
INSERT INTO QuestItems (PlayerID, Slot, Opt, ID)
|
||||
VALUES (?, ?, ?, ?);
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
for (int i = 0; i < AQINVEN_COUNT; i++) {
|
||||
if (player->QInven[i].iID == 0)
|
||||
continue;
|
||||
|
||||
sqlite3_bind_int(stmt, 1, player->iID);
|
||||
sqlite3_bind_int(stmt, 2, i);
|
||||
sqlite3_bind_int(stmt, 3, player->QInven[i].iOpt);
|
||||
sqlite3_bind_int(stmt, 4, player->QInven[i].iID);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// Update Nanos
|
||||
sql = R"(
|
||||
DELETE FROM Nanos WHERE PlayerID = ?;
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, player->iID);
|
||||
sqlite3_step(stmt);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
INSERT INTO Nanos (PlayerID, ID, SKill, Stamina)
|
||||
VALUES (?, ?, ?, ?);
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
for (int i = 0; i < NANO_COUNT; i++) {
|
||||
if (player->Nanos[i].iID == 0)
|
||||
continue;
|
||||
|
||||
sqlite3_bind_int(stmt, 1, player->iID);
|
||||
sqlite3_bind_int(stmt, 2, player->Nanos[i].iID);
|
||||
sqlite3_bind_int(stmt, 3, player->Nanos[i].iSkillID);
|
||||
sqlite3_bind_int(stmt, 4, player->Nanos[i].iStamina);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
// Update Running Quests
|
||||
sql = R"(
|
||||
DELETE FROM RunningQuests WHERE PlayerID = ?;
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
sqlite3_bind_int(stmt, 1, player->iID);
|
||||
sqlite3_step(stmt);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
sql = R"(
|
||||
INSERT INTO RunningQuests
|
||||
(PlayerID, TaskID, RemainingNPCCount1, RemainingNPCCount2, RemainingNPCCount3)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
)";
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
||||
if (player->tasks[i] == 0)
|
||||
continue;
|
||||
sqlite3_bind_int(stmt, 1, player->iID);
|
||||
sqlite3_bind_int(stmt, 2, player->tasks[i]);
|
||||
sqlite3_bind_int(stmt, 3, player->RemainingNPCCount[i][0]);
|
||||
sqlite3_bind_int(stmt, 4, player->RemainingNPCCount[i][1]);
|
||||
sqlite3_bind_int(stmt, 5, player->RemainingNPCCount[i][2]);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Database::updatePlayer(Player *player) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
||||
|
||||
if (!_updatePlayer(player)) {
|
||||
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
void Database::commitTrade(Player *plr1, Player *plr2) {
|
||||
std::lock_guard<std::mutex> lock(dbCrit);
|
||||
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
||||
|
||||
if (!_updatePlayer(plr1)) {
|
||||
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_updatePlayer(plr2)) {
|
||||
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
|
||||
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user