Compare commits
592 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ed41d43a8 | ||
|
|
8595b65421 | ||
|
|
75c78bec4c | ||
|
|
fb56fe7e50 | ||
|
|
34b059fc96 | ||
|
|
088590eca2 | ||
|
|
b3e9fed249 | ||
|
|
4ee8419132 | ||
|
|
5c9b4b7aff | ||
|
|
74e39fb00a | ||
|
|
346965116f | ||
|
|
a3551c8ab8 | ||
|
|
c38018316b | ||
|
|
5acbeb2602 | ||
|
|
32ec15fd1a | ||
|
|
e0508fb29c | ||
|
|
cc241e6325 | ||
|
|
b93e433227 | ||
|
|
c29d8634ad | ||
|
|
66233d2fbe | ||
|
|
877adaf80d | ||
|
|
8740e60e35 | ||
|
|
08a875b69f | ||
|
|
c62be10413 | ||
|
|
a934f07f3a | ||
|
|
03839a2699 | ||
|
|
be411b2684 | ||
|
|
fc83aa151b | ||
|
|
1c75103166 | ||
|
|
7f3837583d | ||
|
|
85cd903586 | ||
|
|
641c35258f | ||
|
|
b19e72b447 | ||
|
|
58f9fa217c | ||
|
|
1066c0ee58 | ||
|
|
2e7cd92a08 | ||
|
|
6750e6b62a | ||
|
|
1e4ada5896 | ||
|
|
b79f02e541 | ||
|
|
08cccf392a | ||
|
|
dac7d90b21 | ||
|
|
cb747a5ae9 | ||
|
|
1897911201 | ||
|
|
54a406501a | ||
|
|
47eaaffa59 | ||
|
|
c3668122d7 | ||
|
|
7b315562ce | ||
|
|
c0ff50296e | ||
|
|
93f227260b | ||
|
|
2c0ab87016 | ||
|
|
de7d68cba1 | ||
|
|
cf7123a5ea | ||
|
|
947b284f39 | ||
|
|
bfa1cb3db4 | ||
|
|
6f2ada7632 | ||
|
|
48c2ce65b2 | ||
|
|
d386cff233 | ||
|
|
9a8beaa7e3 | ||
|
|
69cf3dc8b2 | ||
|
|
18bbcb4e42 | ||
|
|
00e3bcb084 | ||
|
|
bfd161d517 | ||
|
|
5c44fcff96 | ||
|
|
3496897ea8 | ||
|
|
4e82fbe611 | ||
|
|
2433769dbb | ||
|
|
49fe06b3f9 | ||
|
|
563dfa3dc8 | ||
|
|
e4952dc538 | ||
|
|
723c18f18e | ||
|
|
a6247ba387 | ||
|
|
f0aebfcb1f | ||
|
|
2663369222 | ||
|
|
b974a72cd4 | ||
|
|
8babb8980c | ||
|
|
19f699ebd3 | ||
|
|
dff5bb5210 | ||
|
|
5f56b82052 | ||
|
|
d1e76ba627 | ||
|
|
a1a461061d | ||
|
|
afbe865014 | ||
|
|
8b08eeba39 | ||
|
|
1ce460c676 | ||
|
|
19584cfaa9 | ||
|
|
f272e0c360 | ||
|
|
382680b424 | ||
|
|
dc245333da | ||
|
|
04832bb84d | ||
|
|
333d4ceec8 | ||
|
|
819f548a7c | ||
|
|
f0e9df87e2 | ||
|
|
24baf98f09 | ||
|
|
6c9ace34a9 | ||
|
|
53a8d6eec2 | ||
|
|
11351586ca | ||
|
|
a4a0b6ce7e | ||
|
|
4d57785c5b | ||
|
|
c337f6413e | ||
|
|
c5a47a0d86 | ||
|
|
4bd4da8d3b | ||
|
|
248c3ac97e | ||
|
|
ec4f615fa7 | ||
|
|
19456f0e04 | ||
|
|
78b04a0960 | ||
|
|
2b2f8fbd41 | ||
|
|
06bfd236dd | ||
|
|
a7374eeb96 | ||
|
|
9fa2b9ec46 | ||
|
|
e7712a1e34 | ||
|
|
b0c633dc13 | ||
|
|
99eba31456 | ||
|
|
33f9be3b6a | ||
|
|
66eedf1a5c | ||
|
|
427db7782a | ||
|
|
e30f285d6d | ||
|
|
faaac2912c | ||
|
|
3a529dd456 | ||
|
|
956778dbc8 | ||
|
|
6a0fce2238 | ||
|
|
8afb2e2d8b | ||
|
|
3959bd4bcc | ||
|
|
d4ceade3e5 | ||
|
|
c3947e4ac5 | ||
|
|
9a9bd4e957 | ||
|
|
29f691576c | ||
|
|
eb8bfba84d | ||
|
|
2272c8bbb8 | ||
|
|
f192ba4b45 | ||
|
|
b192d72b59 | ||
|
|
6084f28cb9 | ||
|
|
92be16045e | ||
|
|
199c37ab0e | ||
|
|
0d57bbe330 | ||
|
|
71a9297e93 | ||
|
|
beeb940233 | ||
|
|
20048a5082 | ||
|
|
29156c085c | ||
|
|
de20102449 | ||
|
|
1f2827cb22 | ||
|
|
b990ee4044 | ||
|
|
4d9592722a | ||
|
|
aa32ca3570 | ||
|
|
5bdcc011fc | ||
|
|
2fa202d28a | ||
|
|
7dcca6e8d0 | ||
|
|
a5990308a8 | ||
|
|
0974f05d95 | ||
|
|
89f142e795 | ||
|
|
45a0d9f784 | ||
|
|
536cc14019 | ||
|
|
aea202985a | ||
|
|
057f880087 | ||
|
|
dda728119f | ||
|
|
5cbee2dc64 | ||
|
|
be12f49374 | ||
|
|
0afab689ea | ||
|
|
ebab896841 | ||
|
|
9898a0d3f8 | ||
|
|
a2700ce473 | ||
|
|
27f72885ce | ||
|
|
9fab6b22aa | ||
|
|
59f2f3f838 | ||
|
|
2b6fc6d414 | ||
|
|
e0b2a389b3 | ||
|
|
b9ad2441e2 | ||
|
|
2be1050893 | ||
|
|
a89794d076 | ||
|
|
5df4937324 | ||
|
|
9304f8f5e9 | ||
|
|
48017a1f44 | ||
|
|
09bfcb4091 | ||
|
|
58bb2668ca | ||
|
|
cd4dc0eb11 | ||
|
|
ef9efaf5a3 | ||
|
|
9633d880c6 | ||
|
|
e667647ef7 | ||
|
|
0830e30d96 | ||
|
|
10fcbd82b5 | ||
|
|
4da7fa62f4 | ||
|
|
4d12b62812 | ||
|
|
dac441614c | ||
|
|
2277b27adf | ||
|
|
85725d07ea | ||
|
|
dccb54ff2e | ||
|
|
f685bc3ba9 | ||
|
|
281c106acf | ||
|
|
a32ff9d2b1 | ||
|
|
1061dd1fd0 | ||
|
|
8e00d34f1b | ||
|
|
89eea1f36b | ||
|
|
aaff65ad6d | ||
|
|
ce265be842 | ||
|
|
31d47abf23 | ||
|
|
f68f8ec608 | ||
|
|
6b62a625e8 | ||
|
|
0e9a07a4d2 | ||
|
|
6bdf11f382 | ||
|
|
d3250c69d5 | ||
|
|
a3af696f40 | ||
|
|
debb5fe204 | ||
|
|
c74f23c90a | ||
|
|
29db37304b | ||
|
|
8cd3f5bb26 | ||
|
|
f4a8764c8e | ||
|
|
46da027341 | ||
|
|
156dbef90b | ||
|
|
ea3262fc62 | ||
|
|
43600427f2 | ||
|
|
36a9a6b570 | ||
|
|
10e57d5fe8 | ||
|
|
9963ca6b5a | ||
|
|
6ee71767a5 | ||
|
|
061711ff90 | ||
|
|
81450dbb03 | ||
|
|
78a8e3ec00 | ||
|
|
493ad8c52b | ||
|
|
b46ff206b9 | ||
|
|
3c9b35c2ac | ||
|
|
91e694922f | ||
|
|
943ed1e08d | ||
|
|
78ff05ccc9 | ||
|
|
d9a51c1634 | ||
|
|
80357e9fb9 | ||
|
|
2a449ac30c | ||
|
|
54a36f3b97 | ||
|
|
a3002a1fcd | ||
|
|
cd1553fd00 | ||
|
|
9d4d8a4a6e | ||
|
|
98e08f06d5 | ||
|
|
405990cbac | ||
|
|
cc21bb802d | ||
|
|
1102a38c21 | ||
|
|
38c015879f | ||
|
|
cc96adf6cb | ||
|
|
28087734a1 | ||
|
|
a335cf3145 | ||
|
|
a83052e01c | ||
|
|
c8245db796 | ||
|
|
f5645fed00 | ||
|
|
cbb7b72c9b | ||
|
|
b83792e7d6 | ||
|
|
5966c7f746 | ||
|
|
84f0add726 | ||
|
|
fda28199a1 | ||
|
|
3445ad12f5 | ||
|
|
8ed9a6b836 | ||
|
|
8b93088a90 | ||
|
|
2c391a756e | ||
|
|
9f0b7f2a8d | ||
|
|
51e77730c8 | ||
|
|
aa55a010c9 | ||
|
|
e4e293a82b | ||
|
|
f0dd573a01 | ||
|
|
6624e639b8 | ||
|
|
b5f9b88b39 | ||
|
|
9cf0ab1a33 | ||
|
|
0ccb95a2d7 | ||
|
|
ccf5241e82 | ||
|
|
d62e430c84 | ||
|
|
2ee2387e76 | ||
|
|
623dff503c | ||
|
|
3c84db850a | ||
|
|
24dd563b4e | ||
|
|
88327b6fad | ||
|
|
c56db42132 | ||
|
|
d68f04b3df | ||
|
|
8d3eaf641f | ||
|
|
9b23a5949a | ||
|
|
4f4208d481 | ||
|
|
02858e9039 | ||
|
|
17ce7371e6 | ||
|
|
a88702fa9f | ||
|
|
f25c6a1ce7 | ||
|
|
77b003a682 | ||
|
|
4da729bb1f | ||
|
|
d1999d709f | ||
|
|
35c1a0f5a4 | ||
|
|
78980c62bb | ||
|
|
bafde065c3 | ||
|
|
b61eddef13 | ||
|
|
33a027fe64 | ||
|
|
3576641da7 | ||
|
|
8ceecb3273 | ||
|
|
3adc0873c3 | ||
|
|
527dceb499 | ||
|
|
eaf66ca0ad | ||
|
|
0dd24042fb | ||
|
|
8c3efa5790 | ||
|
|
e12bbd6ebb | ||
|
|
d938d46df6 | ||
|
|
b93a77f022 | ||
|
|
ef04c973d0 | ||
|
|
65c11880fa | ||
|
|
03b2a4be3d | ||
|
|
ce5a089c52 | ||
|
|
0ebfef92ba | ||
|
|
22c57bbd3f | ||
|
|
ea1dc2e7a0 | ||
|
|
5527ef6591 | ||
|
|
c5489bf099 | ||
|
|
10317bff2e | ||
|
|
dd45c5a6bd | ||
|
|
451fd809d3 | ||
|
|
8019432868 | ||
|
|
3499b347bf | ||
|
|
f3da6900ad | ||
|
|
7a7434ebf8 | ||
|
|
93fdb23fb1 | ||
|
|
f339e26be1 | ||
|
|
8505ad673f | ||
|
|
3fb9bcb60a | ||
|
|
6d6a3113dc | ||
|
|
67741f8cab | ||
|
|
83f263108b | ||
|
|
4e0f5ebd77 | ||
|
|
492d662a02 | ||
|
|
f267f7063e | ||
|
|
74aa86d336 | ||
|
|
1c1054a5bd | ||
|
|
f793f1db3b | ||
|
|
4c0dd2be06 | ||
|
|
82889f8049 | ||
|
|
f44eee28dd | ||
|
|
1072a97391 | ||
|
|
e27971a444 | ||
|
|
c64c411a58 | ||
|
|
581c7129cd | ||
|
|
7661197339 | ||
|
|
6ee89c0cdd | ||
|
|
7d8123abb9 | ||
|
|
ead80c5edc | ||
|
|
bb15a9eec9 | ||
|
|
35fc001492 | ||
|
|
1bc4735c21 | ||
|
|
7fb6d94b19 | ||
|
|
fa29e84d39 | ||
|
|
c1fe8a1c08 | ||
|
|
30385844a9 | ||
|
|
65465e9965 | ||
|
|
1021f819b5 | ||
|
|
7c4b8e38b8 | ||
|
|
36c873b510 | ||
|
|
c7025ee897 | ||
|
|
d9d653561e | ||
|
|
6a01e4b8a9 | ||
|
|
df81c856be | ||
|
|
10595a0245 | ||
|
|
03dafec059 | ||
|
|
d0c1666c8e | ||
|
|
a5275ed88e | ||
|
|
6ea34a2dad | ||
|
|
ecc77d164d | ||
|
|
cba383f500 | ||
|
|
ce55d33d8e | ||
|
|
95fefd8733 | ||
|
|
5ed6e7b733 | ||
|
|
4f79a37341 | ||
|
|
0ed5e79c6b | ||
|
|
916e32fd10 | ||
|
|
1e2e3797b6 | ||
|
|
272b16c245 | ||
|
|
cf77ae5781 | ||
|
|
99cc018daf | ||
|
|
bfac7687a7 | ||
|
|
0fc913a847 | ||
|
|
52925c8143 | ||
|
|
981d83cd2a | ||
|
|
2feb028f3b | ||
|
|
1b7d7c0231 | ||
|
|
9c6899b615 | ||
|
|
8c17369330 | ||
|
|
01cd9fe16d | ||
|
|
4715434a22 | ||
|
|
31ebca89d2 | ||
|
|
7dfcb3e427 | ||
|
|
c5f445a6a2 | ||
|
|
904a730d18 | ||
|
|
36876e7e0f | ||
|
|
b21a655c23 | ||
|
|
3fd8b6bd2d | ||
|
|
44cc2b0f20 | ||
|
|
b53bdff38a | ||
|
|
25803aa30c | ||
|
|
36feb997ff | ||
|
|
b5296b9af5 | ||
|
|
14221651c0 | ||
|
|
a0621e193d | ||
|
|
728beff686 | ||
|
|
34a3e2ad71 | ||
|
|
f10e3170ae | ||
|
|
f5a04249fb | ||
|
|
794989c852 | ||
|
|
4245d415c3 | ||
|
|
efe7ca43f6 | ||
|
|
f5febb04d7 | ||
|
|
64649ab544 | ||
|
|
fd6df182a8 | ||
|
|
5982de0fa4 | ||
|
|
fb95d79198 | ||
|
|
3d294859df | ||
|
|
e844eea45c | ||
|
|
65a5a50885 | ||
|
|
426394ba81 | ||
|
|
53afb87404 | ||
|
|
3899384745 | ||
|
|
2d529b9663 | ||
|
|
c9813a5985 | ||
|
|
dc3e4d975f | ||
|
|
43b1ca543c | ||
|
|
77a1f32648 | ||
|
|
8127d734b7 | ||
|
|
54c518a417 | ||
|
|
0a7699915d | ||
|
|
0937428c85 | ||
|
|
cf816704c7 | ||
|
|
7d1bca3bd3 | ||
|
|
266041fc29 | ||
|
|
c4f2c9df0d | ||
|
|
5ce2a661bb | ||
|
|
22586063ad | ||
|
|
00d112b74d | ||
|
|
e7ac82b018 | ||
|
|
451fd1ca49 | ||
|
|
6bb8170bde | ||
|
|
3d0e37ab2c | ||
|
|
b13dd1efef | ||
|
|
a6220706a8 | ||
|
|
827c33bd8b | ||
|
|
8e6227bded | ||
|
|
037dae6850 | ||
|
|
883cad72f2 | ||
|
|
d2ae6c6973 | ||
|
|
ca6d3c74a7 | ||
|
|
6a353b9035 | ||
|
|
9887dfe567 | ||
|
|
e204aad1da | ||
|
|
0df640bec5 | ||
|
|
4148935d79 | ||
|
|
6f54c611a9 | ||
|
|
1c3f37bf5a | ||
|
|
ccf69931da | ||
|
|
e837e49be4 | ||
|
|
f91c98da75 | ||
|
|
578f8d0bf5 | ||
|
|
8443fdee32 | ||
|
|
42d799ce46 | ||
|
|
638fbf8bb8 | ||
|
|
efad5f0f2a | ||
|
|
8966b53e02 | ||
|
|
bae1806727 | ||
|
|
0641b7c373 | ||
|
|
44ea473daf | ||
|
|
48a7a0317d | ||
|
|
be6fa2e020 | ||
|
|
98cc1a5d3f | ||
|
|
afb3c05969 | ||
|
|
be5ffb41b8 | ||
|
|
1087057a23 | ||
|
|
b226f5c662 | ||
|
|
bf230d6183 | ||
|
|
b5badf4092 | ||
|
|
093eed3477 | ||
|
|
ce5153c419 | ||
|
|
73cd371f00 | ||
|
|
ae4ef919a1 | ||
|
|
fbeccc542a | ||
|
|
87781a18e4 | ||
|
|
dbd418a5a8 | ||
|
|
4a583239ba | ||
|
|
f84979d86f | ||
|
|
50550ab87e | ||
|
|
943fb6c326 | ||
|
|
78485b5c8e | ||
|
|
37fa9a8d67 | ||
|
|
2e425b05c8 | ||
|
|
e70faeab00 | ||
|
|
a9851b8e31 | ||
|
|
39df5eeb50 | ||
|
|
0986492178 | ||
|
|
6e7b5059da | ||
|
|
f87cd749d3 | ||
|
|
8b9e407f87 | ||
|
|
dbdc92a1bb | ||
|
|
6f1c743b3f | ||
|
|
5eec5dfb41 | ||
|
|
061b45064c | ||
|
|
70bfe4efca | ||
|
|
c32a6be457 | ||
|
|
b0e555e867 | ||
|
|
f1ddad6056 | ||
|
|
495469045d | ||
|
|
1c7a0471e1 | ||
|
|
5c0f7534b8 | ||
|
|
ba0171d5c2 | ||
|
|
08f4488040 | ||
|
|
238f27d967 | ||
|
|
437e7828ac | ||
|
|
43003ac169 | ||
|
|
0d426d05e3 | ||
|
|
b352178c5a | ||
|
|
538fbce505 | ||
|
|
1259c77c11 | ||
|
|
23d4954c11 | ||
|
|
0f1b287d87 | ||
|
|
dcb03c0486 | ||
|
|
5959a95d03 | ||
|
|
af005b7979 | ||
|
|
4043e1aaee | ||
|
|
3ba72706f6 | ||
|
|
317d6852b0 | ||
|
|
d2847bffd2 | ||
|
|
ba3b56247d | ||
|
|
8d8c647032 | ||
|
|
0aa6cc2a27 | ||
|
|
8123c370b1 | ||
|
|
b6979877d7 | ||
|
|
0186048c0b | ||
|
|
753eae06f4 | ||
|
|
022dfc19d4 | ||
|
|
f501052031 | ||
|
|
3ab39fb348 | ||
|
|
7facb894b1 | ||
|
|
7577008b6d | ||
|
|
90a678c550 | ||
|
|
aee7268249 | ||
|
|
622a76ab37 | ||
|
|
450180268a | ||
|
|
8b14517f3f | ||
|
|
947d196891 | ||
|
|
f1f1791aec | ||
|
|
b0bf4ec734 | ||
|
|
bbaead036b | ||
|
|
4734c819b7 | ||
|
|
23ff3f99b0 | ||
|
|
ddd0c85697 | ||
|
|
79e48eaca0 | ||
|
|
b9fdd3f796 | ||
|
|
e0825de506 | ||
|
|
b06208fd2e | ||
|
|
6afd4656af | ||
|
|
d6e812d436 | ||
|
|
f4194fdb0d | ||
|
|
769e7a94ab | ||
|
|
089531c737 | ||
|
|
50f145aee5 | ||
|
|
715cf9dea6 | ||
|
|
9182520f28 | ||
|
|
1018244a22 | ||
|
|
3e04935e47 | ||
|
|
44557e310d | ||
|
|
e19ceea36c | ||
|
|
41856664c4 | ||
|
|
1694186b3d | ||
|
|
75dafde7b2 | ||
|
|
61cb82a00d | ||
|
|
e58d31e69f | ||
|
|
b51087ba81 | ||
|
|
8a09abab3a | ||
|
|
7f60b558cb | ||
|
|
1e348fdbfe | ||
|
|
74afa2179f | ||
|
|
14b253c53d | ||
|
|
082168d7ae | ||
|
|
6d93bf3949 | ||
|
|
45a349d309 | ||
|
|
da7bc410a4 | ||
|
|
ac09369479 | ||
|
|
0a66965c10 | ||
|
|
d9c37f7344 | ||
|
|
bef0e3051e | ||
|
|
da46aa6148 | ||
|
|
073ce141e9 | ||
|
|
26b3efa57a | ||
|
|
23fc3ed8f9 | ||
|
|
0a7dd2c421 | ||
|
|
43f9f69e2b | ||
|
|
18ff6e3378 | ||
|
|
d5a2e5bf49 | ||
|
|
e04b0b95bf | ||
|
|
dcba06570f | ||
|
|
84b4843314 | ||
|
|
04bcdd6ec2 | ||
|
|
850ed0225b | ||
|
|
15598c5896 | ||
|
|
9796f3d823 | ||
|
|
eccd9429c2 | ||
|
|
bf482abbf5 | ||
|
|
fdb9fbd39a | ||
|
|
e677af538d | ||
|
|
d2d5188d5c | ||
|
|
56503a94e6 | ||
|
|
8d7c49c365 |
487 changed files with 165598 additions and 80296 deletions
29
.github/workflows/nightly-release-linux-x64-self-contained.yml
vendored
Normal file
29
.github/workflows/nightly-release-linux-x64-self-contained.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: Nightly Release (Linux x64) (Self-Contained)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '15 7 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Publish Self-Contained
|
||||
run: dotnet publish MHServerEmu.sln --configuration Release --runtime linux-x64 --self-contained
|
||||
- name: Get current date
|
||||
run: echo "DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: MHServerEmu-nightly-${{ env.DATE }}-Release-linux-x64-self-contained
|
||||
path: |
|
||||
./src/MHServerEmu/bin/x64/Release/net8.0/linux-x64/publish
|
||||
!./src/MHServerEmu/bin/x64/Release/net8.0/linux-x64/publish/*.pdb
|
||||
!./src/MHServerEmu/bin/x64/Release/net8.0/linux-x64/publish/*.xml
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -402,3 +402,6 @@ FodyWeavers.xsd
|
|||
.*#
|
||||
.#*
|
||||
|
||||
# Build directory
|
||||
/build
|
||||
|
||||
|
|
|
|||
29
Build.bat
Normal file
29
Build.bat
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
@echo off
|
||||
|
||||
set BUILD_DIR=.\src\MHServerEmu\bin\x64\Release\net8.0
|
||||
set OUTPUT_DIR=.\build
|
||||
|
||||
echo ==================
|
||||
echo Building...
|
||||
echo ==================
|
||||
|
||||
dotnet build MHServerEmu.sln -c Release
|
||||
|
||||
if %errorlevel% neq 0 (
|
||||
echo Build failed!
|
||||
pause
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
echo ==================
|
||||
echo Copying...
|
||||
echo ==================
|
||||
|
||||
if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
|
||||
robocopy "%BUILD_DIR%" "%OUTPUT_DIR%" *.* /s /xf *.pdb *.xml /np /njs /njh
|
||||
|
||||
echo ==================
|
||||
echo Build Complete
|
||||
echo ==================
|
||||
|
||||
pause
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# MHServerEmu Contribution Guidelines
|
||||
|
||||
This is a reverse engineering project, and the game we are dealing with heavily relies on certain aspects being 100% the same between the client and the server. This is both a blessing and a curse: on one hand, we have access to a lot of data and logic that traditionally would be server-only; on the other hand, we cannot significantly deviate from existing implementations without unexpected side effects. We may be able to introduce more significant deviations as the project matures, but this is where things stand right now.
|
||||
|
||||
For this reason, most gameplay-related changes have to be made with the client in mind, and contributors are expected to be using IDA, Ghidra, or other similar disassembler software. For static analysis you should be disassembling the Mac executable, even if you are running on Windows, because the former contains debug symbols for all class and function names that are not present in the other version.
|
||||
|
||||
If this is something you would be able to help with, please contact us on [Discord](https://discord.gg/hjR8Bj52t3), so that we can discuss your potential contributions. If you believe you can contribute in some other way that does not directly involve reverse engineering, we can discuss this as well. We would also be glad to answer any questions about the current state of the codebase.
|
||||
|
||||
**Pull requests without prior discussion are most likely going to be disregarded. You have been warned.**
|
||||
28
CREDITS.md
28
CREDITS.md
|
|
@ -1,6 +1,6 @@
|
|||
# MHServerEmu Thanks/Credits file
|
||||
|
||||
## Contributors
|
||||
## Developers
|
||||
|
||||
- AlexBond
|
||||
|
||||
|
|
@ -8,18 +8,34 @@
|
|||
|
||||
- Kawaikikinou
|
||||
|
||||
- SirLimbo
|
||||
|
||||
- yn01
|
||||
|
||||
## Discord MVPs
|
||||
|
||||
Our Discord server MVPs are not directly involved with the development of MHServerEmu, but they all helped the Marvel Heroes community in various ways.
|
||||
|
||||
- Astrid
|
||||
|
||||
- Doods
|
||||
|
||||
- DrUnkenMonk
|
||||
|
||||
- FF_Lowthor
|
||||
|
||||
- MonEll
|
||||
|
||||
- Pyrox
|
||||
|
||||
- Rylok
|
||||
|
||||
- SinisterSpatula
|
||||
|
||||
## Special Thanks
|
||||
|
||||
- Denys Smirnov for his [protod](https://github.com/dennwc/protod) tool that has been invaluable for reverse engineering the network protocol.
|
||||
|
||||
- mooege and diiis for inspiration.
|
||||
|
||||
- All the Discord MVPs and taskmasters who help us.
|
||||
|
||||
- Black Cat for her abilities of math and cheating death.
|
||||
|
||||
- All the people who worked on the game in any capacity.
|
||||
- All the Gazillion developers who worked on the game in any capacity.
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.Games", "src\MH
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.PlayerManagement", "src\MHServerEmu.PlayerManagement\MHServerEmu.PlayerManagement.csproj", "{33518D5D-07A0-4D08-9013-6CF873CC925B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.Auth", "src\MHServerEmu.Auth\MHServerEmu.Auth.csproj", "{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.Billing", "src\MHServerEmu.Billing\MHServerEmu.Billing.csproj", "{BDAA0D9D-1DDE-4F9A-84ED-FA551A4D4419}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.WebFrontend", "src\MHServerEmu.WebFrontend\MHServerEmu.WebFrontend.csproj", "{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.Leaderboards", "src\MHServerEmu.Leaderboards\MHServerEmu.Leaderboards.csproj", "{2C74480B-B373-4DFD-9C9C-E652E889E9C1}"
|
||||
EndProject
|
||||
|
|
@ -67,10 +65,6 @@ Global
|
|||
{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}.Debug|x64.Build.0 = Debug|x64
|
||||
{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}.Release|x64.ActiveCfg = Release|x64
|
||||
{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}.Release|x64.Build.0 = Release|x64
|
||||
{BDAA0D9D-1DDE-4F9A-84ED-FA551A4D4419}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{BDAA0D9D-1DDE-4F9A-84ED-FA551A4D4419}.Debug|x64.Build.0 = Debug|x64
|
||||
{BDAA0D9D-1DDE-4F9A-84ED-FA551A4D4419}.Release|x64.ActiveCfg = Release|x64
|
||||
{BDAA0D9D-1DDE-4F9A-84ED-FA551A4D4419}.Release|x64.Build.0 = Release|x64
|
||||
{2C74480B-B373-4DFD-9C9C-E652E889E9C1}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{2C74480B-B373-4DFD-9C9C-E652E889E9C1}.Debug|x64.Build.0 = Debug|x64
|
||||
{2C74480B-B373-4DFD-9C9C-E652E889E9C1}.Release|x64.ActiveCfg = Release|x64
|
||||
|
|
|
|||
32
README.md
32
README.md
|
|
@ -6,8 +6,6 @@ The only currently supported version of the game client is **1.52.0.1700** (also
|
|||
|
||||
We post development progress reports on our [blog](https://crypto137.github.io/MHServerEmu/). You can find additional information on various topics in the [documentation](./docs/Index.md). If you would like to discuss this project and/or help with its development, feel free to join our [Discord](https://discord.gg/hjR8Bj52t3).
|
||||
|
||||
**Please make sure to read our [contribution guidelines](./CONTRIBUTING.md) if you would like to participate in the development of this project.**
|
||||
|
||||
## Download
|
||||
|
||||
We provide two kinds of builds: stable and nightly.
|
||||
|
|
@ -32,25 +30,13 @@ You can always upgrade from stable to nightly simply by downloading the latest n
|
|||
|
||||
[](https://nightly.link/Crypto137/MHServerEmu/workflows/nightly-release-windows-x64/master?preview) [](https://nightly.link/Crypto137/MHServerEmu/workflows/nightly-release-linux-x64/master?preview)
|
||||
|
||||
## Features
|
||||
|
||||
MHServerEmu is feature-complete as a single player experience, and we are actively working on getting the remaining multiplayer features up and running:
|
||||
|
||||
- Store Gifting
|
||||
|
||||
- Supergroups
|
||||
|
||||
- Matchmaking
|
||||
|
||||
- PvP
|
||||
|
||||
- Trade Window
|
||||
|
||||
You can find up to date information on what we are working on in [our roadmap](https://github.com/users/Crypto137/projects/5).
|
||||
|
||||
## FAQ
|
||||
|
||||
**Where can I download the game?**
|
||||
**Is the game fully playable?**
|
||||
|
||||
All systems and content that were in the game when it was shut down in 2017 have been restored.
|
||||
|
||||
**Where can I download the game client?**
|
||||
|
||||
We do not provide download links for the game client for legal reasons. If you have played the game through Steam when it was live, you should be able to download it in your Steam library.
|
||||
|
||||
|
|
@ -58,13 +44,11 @@ We do not provide download links for the game client for legal reasons. If you h
|
|||
|
||||
Download the latest stable or nightly build and overwrite your existing files. Nightly builds can be potentially unstable, so it is recommended to back up your account database file located in `MHServerEmu\Data\Account.db` before updating.
|
||||
|
||||
**Will there be any wipes?**
|
||||
|
||||
We plan to force a fresh start when version 1.0 comes out in early 2026. Your data will not be deleted, but it will no longer be compatible with the server. You will be able to continue using your existing data on whatever the last 0.x version is going to be.
|
||||
|
||||
**Are you going to support other versions of the game, like the ones from before the Biggest Update Ever (BUE) came out?**
|
||||
|
||||
Yes, we do plan to implement support for other versions of the game after 1.52 is fully restored. The final pre-BUE version (1.48) has the highest priority.
|
||||
Yes, we do plan to implement support for other versions, including the final pre-BUE version (1.48) from late 2016. Currently there are no timeframes for when this is going to happen. The current work-in-progress 1.48 code is available on the [v48](https://github.com/Crypto137/MHServerEmu/tree/v48) branch.
|
||||
|
||||
Some early work has also been done to support version 1.10 from mid 2013. You can find the code for it in the [MHServerEmu2013](https://github.com/Crypto137/MHServerEmu2013) repository.
|
||||
|
||||
**Are you going to add new content to the game (heroes, team-ups, powers, etc.)?**
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Some of these parameters can be set automatically by using [Bifrost](https://git
|
|||
|
||||
These require further checking for compatibility with various versions of the client.
|
||||
|
||||
### ClientApp::Initialize
|
||||
### ClientApp::Initialize()
|
||||
|
||||
| Parameter | Description | Values |
|
||||
| ----------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
|
|
@ -42,7 +42,7 @@ These require further checking for compatibility with various versions of the cl
|
|||
| -novalidate | | |
|
||||
| -nohardwareinfo | | |
|
||||
|
||||
### ClientAppSettingsPostCoreInit::Initialize
|
||||
### ClientAppSettingsPostCoreInit::Initialize()
|
||||
|
||||
| Parameter | Description | Values |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------- | ------------ |
|
||||
|
|
@ -86,7 +86,7 @@ These require further checking for compatibility with various versions of the cl
|
|||
| -streaming | | |
|
||||
| -platform= | | orbis, dingo |
|
||||
|
||||
### SiteConfig::ParseCommandLine
|
||||
### SiteConfig::ParseCommandLine()
|
||||
|
||||
| Parameter | Description | Values |
|
||||
| ------------ | ----------- | ------ |
|
||||
|
|
@ -94,44 +94,50 @@ These require further checking for compatibility with various versions of the cl
|
|||
| -authserver= | | |
|
||||
| -authurl= | | |
|
||||
|
||||
### ClientGame::Initialize
|
||||
### ClientGame::Initialize()
|
||||
|
||||
| Parameter | Description | Values |
|
||||
| ----------- | ----------- | ------ |
|
||||
| -nooverwolf | | |
|
||||
|
||||
### Atlas::Initialize
|
||||
### Atlas::Initialize()
|
||||
|
||||
| Parameter | Description | Values |
|
||||
| ------------- | ----------- | ------ |
|
||||
| -nomapmarkers | | |
|
||||
|
||||
### AudioManager::Initialize
|
||||
### AudioManager::Initialize()
|
||||
|
||||
| Parameter | Description | Values |
|
||||
| -------------- | ----------- | ------ |
|
||||
| -nomusic | | |
|
||||
| -foleyineditor | | |
|
||||
|
||||
### UnrealAudioManager::Initialize
|
||||
### UnrealAudioManager::Initialize()
|
||||
|
||||
| Parameter | Description | Values |
|
||||
| ------------------ | ----------- | ------ |
|
||||
| -audioglobalfocus | | |
|
||||
| -audiofilepackages | | |
|
||||
|
||||
### UnrealGameAdapter::Initialize
|
||||
### UnrealGameAdapter::Initialize()
|
||||
|
||||
| Parameter | Description | Values |
|
||||
| ---------------- | ----------- | ------ |
|
||||
| -dependencygraph | | |
|
||||
|
||||
### UnrealGameAdapter::SendSteamUserInfo
|
||||
### UnrealGameAdapter::SendSteamUserInfo()
|
||||
|
||||
| Parameter | Description | Values |
|
||||
| ----------------------- | ----------- | ------ |
|
||||
| -steamachievementupdate | | |
|
||||
|
||||
### InitGazillionSystems()
|
||||
|
||||
| Parameter | Description | Values |
|
||||
| --------- | ------------------------ | ------------------------------------------------------------------------------ |
|
||||
| -locale= | Overrides client locale. | Name of locale file under `Data\Game\Loco` without extension (e.g. `eng.all`). |
|
||||
|
||||
## Logging Channels
|
||||
|
||||
You can specify channels for verbose logging using the following syntax: `-LoggingChannels=-ALL,+GAME`. Available channels:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ If this is your first time using MHServerEmu, you may be interested in this.
|
|||
MHServerEmu-specific topics.
|
||||
|
||||
- [Server Commands](./ServerEmu/ServerCommands.md) - a list of available server commands.
|
||||
- [Web API](./ServerEmu/WebApi.md) - web API documentation.
|
||||
|
||||
## Game
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Server Commands
|
||||
|
||||
This list was automatically generated on `2025.08.16 18:28:24 UTC` using server version `0.7.0`.
|
||||
This list was automatically generated on `2026.03.25 14:52:06 UTC` using server version `1.0.0`.
|
||||
|
||||
To see an up to date list of all commands, type !commands in the server console or the in-game chat. When invoking a command from in-game your account has to meet the user level requirement for the command.
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ Account management commands.
|
|||
| !account ban [email] | Bans the specified account. | Moderator | Any |
|
||||
| !account create [email] [playerName] [password] | Creates a new account. | Any | Any |
|
||||
| !account download | Downloads a JSON copy of the current account. | Any | Client |
|
||||
| !account info | Shows information for the logged in account. | Any | Client |
|
||||
| !account info | Shows information for the logged in account. | Moderator | Client |
|
||||
| !account password [email] [password] | Changes password for the specified account. | Any | Any |
|
||||
| !account playername [email] [playername] | Changes player name for the specified account. | Any | Any |
|
||||
| !account unban [email] | Unbans the specified account. | Moderator | Any |
|
||||
|
|
@ -24,9 +24,10 @@ Account management commands.
|
|||
## Achievement
|
||||
Commands related to the achievement system.
|
||||
|
||||
| Command | Description | User Level | Invoker Type |
|
||||
| ---------------------- | ------------------------------------------- | ---------- | ------------ |
|
||||
| !achievement info [id] | Outputs info for the specified achievement. | Admin | Any |
|
||||
| Command | Description | User Level | Invoker Type |
|
||||
| ---------------------- | ------------------------------------------------------- | ---------- | ------------ |
|
||||
| !achievement info [id] | Outputs info for the specified achievement. | Admin | Any |
|
||||
| !achievement localeid | Generates a LocaleStringId from the specified argument. | Admin | Any |
|
||||
|
||||
## AOI
|
||||
Commands for interacting with the invoker player's area of interest (AOI).
|
||||
|
|
@ -44,6 +45,8 @@ Commands for boosting the stats of the invoker player's current avatar.
|
|||
| Command | Description | User Level | Invoker Type |
|
||||
| ----------------------- | --------------------------------------------------- | ---------- | ------------ |
|
||||
| !boost damage [1-10000] | Sets DamagePctBonus for the current avatar. | Admin | Client |
|
||||
| !boost invulnerable | Switches Invulnerable for the current avatar. | Admin | Client |
|
||||
| !boost mana | Switches NoEnduranceCosts for the current avatar. | Admin | Client |
|
||||
| !boost vsboss [1-10000] | Sets DamagePctBonusVsBosses for the current avatar. | Admin | Client |
|
||||
|
||||
## Client
|
||||
|
|
@ -105,7 +108,7 @@ Commands for managing items.
|
|||
| Command | Description | User Level | Invoker Type |
|
||||
| ---------------------------- | -------------------------------------------------------------------------- | ---------- | ------------ |
|
||||
| !item cleardeliverybox | Destroys all items contained in the delivery box inventory. | Any | Client |
|
||||
| !item creditchest | Converts 500k credits to a sellable chest item. | Any | Client |
|
||||
| !item creditchest | Converts credits to a sellable chest item. | Any | Client |
|
||||
| !item destroyindestructible | Destroys indestructible items contained in the player's general inventory. | Any | Client |
|
||||
| !item drop [pattern] [count] | Creates and drops the specified item from the current avatar. | Admin | Client |
|
||||
| !item give [pattern] [count] | Creates and gives the specified item to the current player. | Admin | Client |
|
||||
|
|
@ -208,20 +211,33 @@ Region management commands.
|
|||
## Server
|
||||
Server management commands.
|
||||
|
||||
| Command | Description | User Level | Invoker Type |
|
||||
| ------------------------ | ----------------------------------------- | ---------- | ------------- |
|
||||
| !server broadcast | Broadcasts a notification to all players. | Admin | Any |
|
||||
| !server reloadlivetuning | Reloads live tuning settings. | Admin | ServerConsole |
|
||||
| !server shutdown | Shuts the server down. | Admin | Any |
|
||||
| !server status | Prints server status. | Any | Any |
|
||||
| Command | Description | User Level | Invoker Type |
|
||||
| --------------------------------- | ------------------------------------------------- | ---------- | ------------- |
|
||||
| !server broadcast | Broadcasts a notification to all players. | Admin | Any |
|
||||
| !server reloadaddg | Reloads the Add G page. | Admin | ServerConsole |
|
||||
| !server reloadcatalog | Reloads MTX store catalog. | Admin | ServerConsole |
|
||||
| !server reloaddashboard | Reloads the web dashboard. | Admin | ServerConsole |
|
||||
| !server reloadlivetuning | Reloads live tuning settings. | Admin | ServerConsole |
|
||||
| !server reloadplayernameblacklist | Reloads the player name blacklist. | Admin | ServerConsole |
|
||||
| !server shutdown | Shuts the server down. | Admin | Any |
|
||||
| !server status | Prints server status. | Any | Any |
|
||||
| !server whitelist | Enables or disables account whitelist for logins. | Admin | ServerConsole |
|
||||
|
||||
## Store
|
||||
Commands for interacting with the in-game store.
|
||||
|
||||
| Command | Description | User Level | Invoker Type |
|
||||
| -------------------- | --------------------------------------------------------------- | ---------- | ------------ |
|
||||
| !store addg [amount] | Adds the specified number of Gs to this account. | Admin | Client |
|
||||
| !store convertes | Converts 100 Eternity Splinters to the equivalent amount of Gs. | Any | Client |
|
||||
| Command | Description | User Level | Invoker Type |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------- | ------------ |
|
||||
| !store addg [amount] | Adds the specified number of Gs to this account. | Admin | Client |
|
||||
| !store convertes [amount] | Converts Eternity Splinters to the equivalent amount of Gs. Defaults to 100 Eternity Splinters if no value is specified. | Any | Client |
|
||||
|
||||
## UltimatePrestige
|
||||
Ultimate Prestige system commands.
|
||||
|
||||
| Command | Description | User Level | Invoker Type |
|
||||
| -------------------------- | ----------------------------------------------------- | ---------- | ------------ |
|
||||
| !ultimateprestige activate | Activates the Ultimate Prestige for the current hero. | Any | Client |
|
||||
| !ultimateprestige level | Prints the current Ultimate Prestige level. | Any | Client |
|
||||
|
||||
## Unlock
|
||||
Commands for unlocking various things.
|
||||
|
|
@ -231,6 +247,14 @@ Commands for unlocking various things.
|
|||
| !unlock chapters | Unlocks all chapters. | Admin | Client |
|
||||
| !unlock waypoints | Unlocks all waypoints. | Admin | Client |
|
||||
|
||||
## WebApi
|
||||
Web API management commands.
|
||||
|
||||
| Command | Description | User Level | Invoker Type |
|
||||
| ------------------- | ---------------------------- | ---------- | ------------- |
|
||||
| !webapi generatekey | Generates a new web API key. | Admin | ServerConsole |
|
||||
| !webapi reloadkeys | Reloads web API keys | Admin | ServerConsole |
|
||||
|
||||
## Misc
|
||||
|
||||
| Command | Description | User Level | Invoker Type |
|
||||
|
|
@ -241,6 +265,7 @@ Commands for unlocking various things.
|
|||
| !help | Help needs no help. | Any | Any |
|
||||
| !jail | Teleports to East Side: Detention Facility (old). | Admin | Client |
|
||||
| !position | Shows current position. | Any | Client |
|
||||
| !syncmana | Syncs the current mana value with the server. | Any | Client |
|
||||
| !tower | Teleports to Avengers Tower (original). | Any | Client |
|
||||
| !tp | Teleports to position. Usage: tp x:+1000 (relative to current position) tp x100 y500 z10 (absolute position) | Admin | Client |
|
||||
|
||||
|
|
|
|||
239
docs/ServerEmu/WebApi.md
Normal file
239
docs/ServerEmu/WebApi.md
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
# Web API
|
||||
|
||||
MHServerEmu can provide web API functionality as part of its web frontend. It is enabled by default, and the server listens for requests on `http://localhost:8080/`. This can be customized in the `WebFrontend` section of `Config.ini`.
|
||||
|
||||
The web API uses JSON for serialization. Requests return JSON as output and expect JSON as input in the request's body when needed.
|
||||
|
||||
Some endpoints are restricted and require an API key to access.
|
||||
|
||||
- API keys can be generated using the `!webapi generatekey` command.
|
||||
|
||||
- API keys are saved to `Data/Web/ApiKeys.json`. Keys can be reloaded from this file at runtime using the `!webapi reloadkeys` command.
|
||||
|
||||
- API keys are not logged, the only way to access generated keys is by reading them from the `ApiKeys.json` file.
|
||||
|
||||
- Each API key has a specific access type assigned to it (e.g. `AccountManagement`), which limits its functionality to a specific domain.
|
||||
|
||||
- API keys need to be provided when making requests to restricted endpoints by adding them as an `Authorization` HTTP request header with the `Bearer` scheme (e.g. `Authorization: Bearer FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF`).
|
||||
|
||||
Below is reference information on available endpoints.
|
||||
|
||||
## Account Management
|
||||
|
||||
### /AccountManagement/Create
|
||||
|
||||
#### POST
|
||||
|
||||
Requests the server to create an account with the provided data.
|
||||
|
||||
- Access: `None`
|
||||
|
||||
- Input Fields:
|
||||
|
||||
- `string Email`
|
||||
|
||||
- `string PlayerName`
|
||||
|
||||
- `string Password`
|
||||
|
||||
- Output Fields:
|
||||
|
||||
- `int Result`
|
||||
|
||||
### /AccountManagement/SetPlayerName
|
||||
|
||||
#### POST
|
||||
|
||||
Requests the server to change the player name of an existing account.
|
||||
|
||||
- Access: `AccountManagement`
|
||||
|
||||
- Input Fields:
|
||||
|
||||
- `string Email`
|
||||
|
||||
- `string PlayerName`
|
||||
|
||||
- Output Fields:
|
||||
|
||||
- `int Result`
|
||||
|
||||
### /AccountManagement/SetPassword
|
||||
|
||||
#### POST
|
||||
|
||||
Requests the server to change the password of an existing account.
|
||||
|
||||
- Access: `AccountManagement`
|
||||
|
||||
- Input Fields:
|
||||
|
||||
- `string Email`
|
||||
|
||||
- `string Password`
|
||||
|
||||
- Output Fields:
|
||||
|
||||
- `int Result`
|
||||
|
||||
### /AccountManagement/SetUserLevel
|
||||
|
||||
#### POST
|
||||
|
||||
Requests the server to change the user level of an existing account.
|
||||
|
||||
- Access: `AccountManagement`
|
||||
|
||||
- Input Fields:
|
||||
|
||||
- `string Email`
|
||||
|
||||
- `byte UserLevel`
|
||||
|
||||
- Output Fields:
|
||||
|
||||
- `int Result`
|
||||
|
||||
### /AccountManagement/SetFlag
|
||||
|
||||
#### POST
|
||||
|
||||
Requests the server to set a flag on an existing account.
|
||||
|
||||
- Access: `AccountManagement`
|
||||
|
||||
- Input Fields:
|
||||
|
||||
- `string Email`
|
||||
|
||||
- `int Flags`
|
||||
|
||||
- Output Fields:
|
||||
|
||||
- `int Result`
|
||||
|
||||
Account Flag Reference:
|
||||
|
||||
```csharp
|
||||
enum AccountFlags
|
||||
{
|
||||
None = 0,
|
||||
IsBanned = 1 << 0,
|
||||
IsArchived = 1 << 1,
|
||||
IsPasswordExpired = 1 << 2,
|
||||
DEPRECATEDLinuxCompatibilityMode = 1 << 3,
|
||||
IsWhitelisted = 1 << 4,
|
||||
}
|
||||
```
|
||||
|
||||
### /AccountManagement/ClearFlag
|
||||
|
||||
#### POST
|
||||
|
||||
Requests the server to clear a flag on an existing account.
|
||||
|
||||
- Access: `AccountManagement`
|
||||
|
||||
- Input Fields:
|
||||
|
||||
- `string Email`
|
||||
|
||||
- `int Flags`
|
||||
|
||||
- Output Fields:
|
||||
|
||||
- `int Result`
|
||||
|
||||
See `/AccountManagement/SetFlag` above for flag reference.
|
||||
|
||||
## Server Status
|
||||
|
||||
### /ServerStatus
|
||||
|
||||
#### GET
|
||||
|
||||
Retrieves server status information.
|
||||
|
||||
- Access: `None`
|
||||
|
||||
- Output:
|
||||
|
||||
- `Dictionary<string, long>`
|
||||
|
||||
## Region Report
|
||||
|
||||
### /RegionReport
|
||||
|
||||
#### GET
|
||||
|
||||
Retrieves server region information.
|
||||
|
||||
- Access: `None`
|
||||
|
||||
- Output Fields:
|
||||
|
||||
- `List<RegionReport.Entry> Regions`
|
||||
|
||||
- `string GameId`
|
||||
|
||||
- `string RegionId`
|
||||
|
||||
- `string Name`
|
||||
|
||||
- `string DifficultyTier`
|
||||
|
||||
- `string Uptime`
|
||||
|
||||
## Metrics
|
||||
|
||||
### /Metrics/Performance
|
||||
|
||||
#### GET
|
||||
|
||||
Retrieves server performance metrics.
|
||||
|
||||
- Access: `None`
|
||||
|
||||
- Output Fields:
|
||||
|
||||
- `string Id`
|
||||
|
||||
- `MemoryMetrics.Report Memory`
|
||||
|
||||
- `long GCIndex`
|
||||
|
||||
- `long GCCountGen0`
|
||||
|
||||
- `long GCCountGen1`
|
||||
|
||||
- `long TotalCommittedBytes`
|
||||
|
||||
- `long HeapSizeBytes`
|
||||
|
||||
- `double PauseTimePercentage`
|
||||
|
||||
- `MetricTracker.ReportEntry PauseDuration`
|
||||
|
||||
- `Dictionary<ulong, GamePerformanceMetrics.Report> Games`
|
||||
|
||||
- `MetricTracker.ReportEntry UpdateTime`
|
||||
|
||||
- `MetricTracker.ReportEntry FrameTime`
|
||||
|
||||
- `MetricTracker.ReportEntry ScheduledEventsPerUpdate`
|
||||
|
||||
- `MetricTracker.ReportEntry EntityCount`
|
||||
|
||||
- `MetricTracker.ReportEntry PlayerCount`
|
||||
|
||||
- `MetricTracker.ReportEntry` fields:
|
||||
|
||||
- `float Average`
|
||||
|
||||
- `float Median`
|
||||
|
||||
- `float Last`
|
||||
|
||||
- `float Min`
|
||||
|
||||
- `float Max`
|
||||
|
|
@ -41,16 +41,30 @@ Setting the server up for connections outside of your local network requires the
|
|||
|
||||
## Managing Accounts
|
||||
|
||||
You can create and manage accounts by using ! commands in the server console or the in-game chat window. Here are some commands to get you started:
|
||||
You can create and manage accounts by using `!` commands in the server console or the in-game chat window. Here are some commands to get you started:
|
||||
|
||||
- `!account create [email] [playerName] [password]` - creates a new account with the specified email, player name, and password. Email and player name must be unique for each account.
|
||||
|
||||
- `!account userlevel [0|1|2]` - sets user level for the specified account to user (0), moderator (1), or admin (2). Higher user levels enable additional in-game command privileges, up to being able to manage other accounts and shut down the server.
|
||||
- `!account userlevel [email] [0|1|2]` - sets user level for the specified account to user (0), moderator (1), or admin (2). Higher user levels enable additional in-game command privileges, up to being able to manage other accounts and shut down the server.
|
||||
|
||||
- `!account password [email] [newPassword]` - changes password for the specified account.
|
||||
|
||||
For a more in-depth list of commands see [Server Commands](./../ServerEmu/ServerCommands.md) or type `!commands`.
|
||||
|
||||
## Enabling Server Garbage Collection
|
||||
|
||||
When hosting a server for larger player counts (50+), it is recommended to enable .NET's server garbage collection mode.
|
||||
|
||||
The easiest way to enable it for MHServerEmu is to modify the `MHServerEmu.runtimeconfig.json` file located next to `MHServerEmu.exe`:
|
||||
|
||||
1. Open `MHServerEmu.runtimeconfig.json` with a text editor.
|
||||
|
||||
2. Add the following line to the `configProperties` section: `"System.GC.Server": true`.
|
||||
|
||||
Please keep in mind that the server garbage collection mode tends to follow the "any RAM not used is RAM wasted" approach, which can result in very heavy RAM usage, especially when the server stays up for longer periods of time. You may want to limit memory usage by adding the following line to the same `configProperties` section of `MHServerEmu.runtimeconfig.json`: `"System.GC.HeapHardLimitPercent": 80` (this will limit usage to 80% of available RAM).
|
||||
|
||||
You can find out more about the differences between the workstation (default) and the server modes in the [.NET documentation](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/workstation-server-gc).
|
||||
|
||||
## Setting Up Live Tips
|
||||
|
||||
The client can download additional loading screen tips from the server.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ The following instructions are intended for stable builds of the server. If you
|
|||
|
||||
1. Run `StartServer.bat` and wait for MHServerEmu to initialize. There should be two server windows, one of them should be minimized by default and blank.
|
||||
|
||||
2. (Optional) Open http://localhost:8080/AccountManagement/Create and create an account. Please note that this link is going to work only when the server is running.
|
||||
2. (Optional) Open [http://localhost:8080/Dashboard/](http://localhost:8080/Dashboard/) and create an account. Please note that this link is going to work only when the server is running.
|
||||
|
||||
3. Run `StartClient.bat` and log in with your created account OR run `StartClientAutoLogin.bat` to play with a default account.
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ Now you can actually start everything and get in-game.
|
|||
|
||||
2. Start MHServerEmu and wait for it to load.
|
||||
|
||||
3. Open the following link in your web browser and create your account: [http://localhost:8080/AccountManagement/Create](http://localhost:8080/AccountManagement/Create). This link is going to work only when MHServerEmu is fully up and running.
|
||||
3. Open the following link in your web browser and create your account: [http://localhost:8080/Dashboard/](http://localhost:8080/Dashboard/). This link is going to work only when MHServerEmu is fully up and running.
|
||||
|
||||
4. Launch the game with the following argument: `-siteconfigurl=localhost/SiteConfig.xml`.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
# Embedded Browser
|
||||
|
||||
Some of the in-game UI panels (TOS popup on login, store, community news) use a CEF-based web browser that can load HTML pages. These pages can interact with the UI via a JavaScript API.
|
||||
Some of the in-game UI panels (TOS popup on login, store, community news) use an embedded web browser that can load HTML pages. These pages can interact with the client UI via a JavaScript API.
|
||||
|
||||
## Browser Versions
|
||||
|
||||
Originally the client used Awesomium as the browser backend, but it was replaced with Chromium Embedded Framework (CEF) in game version 1.22. Below is a list of known browser versions used by the client.
|
||||
|
||||
| Game Version | Browser Version | Browser Release Date |
|
||||
| ------------ | ------------------------ | -------------------- |
|
||||
| 1.9-1.21 | Awesomium 1.6.5 | 2012-02-23 |
|
||||
| 1.22-1.32 | CEF 3.1650.1544 | 2013-12-08 |
|
||||
| 1.33 | CEF 3.2272.2035 | 2015-02-26 |
|
||||
| 1.34 | CEF 3.2272.2077 | 2015-04-13 |
|
||||
| 1.35-1.52 | CEF 3.1650.1639 | 2014-03-13 |
|
||||
| 1.53 | CEF 3.3112.1656.g9ec3e42 | 2017-08-10 |
|
||||
|
||||
## API Calls
|
||||
|
||||
|
|
@ -50,3 +63,11 @@ The actual viewable area is slightly smaller than these.
|
|||
- Community News (Version 2) Main Page 988x644
|
||||
|
||||
- Community News (Version 2) Popup: 650x764
|
||||
|
||||
## Bundles
|
||||
|
||||
- Bundle images are downloaded and cached in `%TEMP%\MarvelHeroes`. It is possible for the client to cache an invalid bundle image, which may require clearing the cache to fix it.
|
||||
|
||||
- A bundle image needs to be a PNG file with its horizontal and vertical resolution being a multiple of 4. Preferred resolution is 344x128.
|
||||
|
||||
- `?gmode=` is appended to information page requests, indicating whether the gifting mode is enabled (0 or 1).
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
using MHServerEmu.Core.Config;
|
||||
|
||||
namespace MHServerEmu.Auth
|
||||
{
|
||||
public class AuthConfig : ConfigContainer
|
||||
{
|
||||
public string Address { get; private set; } = "localhost";
|
||||
public string Port { get; private set; } = "8080";
|
||||
public bool EnableWebApi { get; private set; } = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
using System.Net;
|
||||
using MHServerEmu.Auth.Handlers;
|
||||
using MHServerEmu.Core.Config;
|
||||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Network;
|
||||
|
||||
namespace MHServerEmu.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles HTTP requests from clients.
|
||||
/// </summary>
|
||||
public class AuthServer : IGameService
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
private readonly string _url;
|
||||
private readonly AuthProtobufHandler _protobufHandler;
|
||||
private readonly AuthWebApiHandler _webApiHandler;
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private HttpListener _listener;
|
||||
|
||||
public GameServiceState State { get; private set; } = GameServiceState.Created;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="AuthServer"/> instance.
|
||||
/// </summary>
|
||||
public AuthServer()
|
||||
{
|
||||
var config = ConfigManager.Instance.GetConfig<AuthConfig>();
|
||||
|
||||
_url = $"http://{config.Address}:{config.Port}/";
|
||||
_protobufHandler = new();
|
||||
|
||||
if (config.EnableWebApi)
|
||||
_webApiHandler = new();
|
||||
}
|
||||
|
||||
#region IGameService Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Runs this <see cref="AuthServer"/> instance.
|
||||
/// </summary>
|
||||
public async void Run()
|
||||
{
|
||||
// Reset CTS
|
||||
_cts?.Dispose();
|
||||
_cts = new();
|
||||
|
||||
// Create an http server and start listening for incoming connections
|
||||
_listener = new HttpListener();
|
||||
_listener.Prefixes.Add(_url);
|
||||
_listener.Start();
|
||||
|
||||
Logger.Info($"AuthServer is listening on {_url}...");
|
||||
State = GameServiceState.Running;
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Wait for a connection, and handle the request
|
||||
HttpListenerContext context = await _listener.GetContextAsync().WaitAsync(_cts.Token);
|
||||
await HandleRequestAsync(context.Request, context.Response);
|
||||
context.Response.Close();
|
||||
}
|
||||
catch (TaskCanceledException) { return; } // Stop handling connections
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error($"Run(): Unhandled exception: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops listening and shuts down this <see cref="AuthServer"/> instance.
|
||||
/// </summary>
|
||||
public void Shutdown()
|
||||
{
|
||||
if (_listener == null) return;
|
||||
if (_listener.IsListening == false) return;
|
||||
|
||||
// Cancel async tasks (listening for context)
|
||||
_cts.Cancel();
|
||||
|
||||
// Close the listener
|
||||
_listener.Close();
|
||||
_listener = null;
|
||||
|
||||
State = GameServiceState.Shutdown;
|
||||
}
|
||||
|
||||
public void ReceiveServiceMessage<T>(in T message) where T : struct, IGameServiceMessage
|
||||
{
|
||||
// AuthServer should not be handling messages from TCP clients
|
||||
switch (message)
|
||||
{
|
||||
default:
|
||||
Logger.Warn($"ReceiveServiceMessage(): Unhandled service message type {typeof(T).Name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetStatus()
|
||||
{
|
||||
if (_listener == null || _listener.IsListening == false)
|
||||
return "Not listening";
|
||||
|
||||
return $"Protobuf Handler: {_protobufHandler != null} | Web API Handler: {_webApiHandler != null}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Routes an <see cref="HttpListenerRequest"/> to the appropriate handler.
|
||||
/// </summary>
|
||||
private async Task HandleRequestAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
bool requestIsFromGameClient = (request.UserAgent == "Secret Identity Studios Http Client");
|
||||
|
||||
// We should be getting only GET and POST
|
||||
switch (request.HttpMethod)
|
||||
{
|
||||
case "GET":
|
||||
if (request.Url.LocalPath == "/favicon.ico") return; // Ignore favicon requests
|
||||
|
||||
// Web API get requests
|
||||
if (requestIsFromGameClient == false && _webApiHandler != null)
|
||||
{
|
||||
await _webApiHandler.HandleRequestAsync(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "POST":
|
||||
// Client auth messages
|
||||
if (requestIsFromGameClient && request.Url.LocalPath == "/Login/IndexPB")
|
||||
{
|
||||
await _protobufHandler.HandleMessageAsync(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Web API post requests
|
||||
if (requestIsFromGameClient == false && _webApiHandler != null)
|
||||
{
|
||||
await _webApiHandler.HandleRequestAsync(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Display a warning for unhandled requests
|
||||
string source = requestIsFromGameClient ? "a game client" : $"an unknown UserAgent ({request.UserAgent})";
|
||||
Logger.Warn($"HandleRequestAsync(): Unhandled {request.HttpMethod} to {request.Url.LocalPath} from {source} on {request.RemoteEndPoint}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Create Account</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
html {
|
||||
background-color: #0D0D0B;
|
||||
}
|
||||
|
||||
div.content {
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border: 1px solid #00717c;
|
||||
background-color: #00090E;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
div.content h1, h2, h3, h4, h5, h6 {
|
||||
color: #00aaff;
|
||||
font-family: "Bebas Neue", "Trebuchet MS", Verdana, sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
div.content p {
|
||||
color: #d7d7d7;
|
||||
font: 12px/1.385 Verdana;
|
||||
}
|
||||
|
||||
div.content p.disclaimer {
|
||||
color: red;
|
||||
}
|
||||
|
||||
div.content input {
|
||||
border: 1px solid black;
|
||||
background: rgb(28, 28, 28);
|
||||
box-shadow: 0 0 1px 1px #00aaff;
|
||||
color: #d7d7d7;
|
||||
font: 12px/1.385 Verdana;
|
||||
}
|
||||
|
||||
div.content label {
|
||||
color: #d7d7d7;
|
||||
font-family: "Bebas Neue", "Trebuchet MS", Verdana, sans-serif;
|
||||
font-size: x-large;
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h1>Create Account</h1>
|
||||
<p class="disclaimer">
|
||||
MHServerEmu is alpha software in active development, and it may not be completely secure.</br>
|
||||
Please avoid using your real email / password combinations on public servers.
|
||||
</p>
|
||||
<form method="POST">
|
||||
<label for="email">Email</label><br>
|
||||
<input type="text" id="email" name="email" maxlength="320" required><br><br>
|
||||
<label for="playerName">Player Name</label><br>
|
||||
<input type="text" id="playerName" name="playerName" maxlength="16" required><br><br>
|
||||
<label for="password">Password</label><br>
|
||||
<input type="password" id="password" name="password" minlength="3" maxlength="64" required><br><br>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>%RESPONSE_TITLE%</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
html {
|
||||
background-color: #0D0D0B;
|
||||
}
|
||||
|
||||
div.content {
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border: 1px solid #00717c;
|
||||
background-color: #00090E;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
div.content h1, h2, h3, h4, h5, h6 {
|
||||
color: #00aaff;
|
||||
font-family: "Bebas Neue", "Trebuchet MS", Verdana, sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
div.content p {
|
||||
color: #d7d7d7;
|
||||
font: 12px/1.385 Verdana;
|
||||
}
|
||||
|
||||
li {
|
||||
color: #d7d7d7;
|
||||
font: 12px/1.385 Verdana;
|
||||
}
|
||||
|
||||
table {
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
td {
|
||||
color: #d7d7d7;
|
||||
font: 12px/1.385 Verdana;
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h1>%RESPONSE_TITLE%</h1>
|
||||
<p>%RESPONSE_TEXT%</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
using System.Net;
|
||||
using Gazillion;
|
||||
using Google.ProtocolBuffers;
|
||||
using MHServerEmu.Core.Config;
|
||||
using MHServerEmu.Core.Extensions;
|
||||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Network;
|
||||
using MHServerEmu.PlayerManagement;
|
||||
using MHServerEmu.PlayerManagement.Network;
|
||||
|
||||
namespace MHServerEmu.Auth.Handlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for <see cref="IMessage"/> instances sent to the <see cref="AuthServer"/>.
|
||||
/// </summary>
|
||||
public class AuthProtobufHandler
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
private static readonly bool HideSensitiveInformation = ConfigManager.Instance.GetConfig<LoggingConfig>().HideSensitiveInformation;
|
||||
|
||||
/// <summary>
|
||||
/// Receives and handles an <see cref="IMessage"/>.
|
||||
/// </summary>
|
||||
public async Task HandleMessageAsync(HttpListenerRequest request, HttpListenerResponse response)
|
||||
{
|
||||
MessageBuffer messageBuffer = new(request.InputStream);
|
||||
|
||||
switch ((FrontendProtocolMessage)messageBuffer.MessageId)
|
||||
{
|
||||
case FrontendProtocolMessage.LoginDataPB: await OnLoginDataPB(request, response, messageBuffer); break;
|
||||
case FrontendProtocolMessage.PrecacheHeaders: await OnPrecacheHeaders(request, response, messageBuffer); break;
|
||||
|
||||
default: Logger.Warn($"HandleMessageAsync(): Unhandled {(FrontendProtocolMessage)messageBuffer.MessageId} [{messageBuffer.MessageId}]"); break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a <see cref="LoginDataPB"/> message.
|
||||
/// </summary>
|
||||
private async Task<bool> OnLoginDataPB(HttpListenerRequest httpRequest, HttpListenerResponse httpResponse, MessageBuffer messageBuffer)
|
||||
{
|
||||
LoginDataPB loginDataPB = messageBuffer.Deserialize<FrontendProtocolMessage>() as LoginDataPB;
|
||||
if (loginDataPB == null) return Logger.WarnReturn(false, $"OnLoginDataPB(): Failed to retrieve message");
|
||||
|
||||
// Mask the end point name to prevent sensitive information from appearing in logs in needed
|
||||
string endPointName = HideSensitiveInformation
|
||||
? httpRequest.RemoteEndPoint.ToStringMasked()
|
||||
: httpRequest.RemoteEndPoint.ToString();
|
||||
|
||||
#if DEBUG
|
||||
// Send a TOS popup when the client uses tos@test.com as email
|
||||
if (loginDataPB.EmailAddress == "tos@test.com")
|
||||
{
|
||||
var tosTicket = AuthTicket.CreateBuilder()
|
||||
.SetSessionId(0)
|
||||
.SetTosurl("http://localhost/tos") // The client adds &locale=en_us to this url (or another locale code)
|
||||
.Build();
|
||||
|
||||
await HttpHelper.SendProtobufAsync(httpResponse, tosTicket, (int)AuthStatusCode.NeedToAcceptLegal);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Try to create a new session from the data we received
|
||||
PlayerManagerService playerManager = ServerManager.Instance.GetGameService(GameServiceType.PlayerManager) as PlayerManagerService;
|
||||
if (playerManager == null)
|
||||
return Logger.ErrorReturn(false, $"OnLoginDataPB(): Failed to connect to the player manager");
|
||||
|
||||
AuthStatusCode statusCode = playerManager.OnLoginDataPB(loginDataPB, out AuthTicket ticket);
|
||||
|
||||
// Respond with an error if session creation didn't succeed
|
||||
if (statusCode != AuthStatusCode.Success)
|
||||
{
|
||||
httpResponse.StatusCode = (int)statusCode;
|
||||
return Logger.InfoReturn(true, $"Authentication for the game client on {endPointName} failed ({statusCode})");
|
||||
}
|
||||
|
||||
// Send an AuthTicket if we were able to create a session
|
||||
string machineId = loginDataPB.HasMachineId ? loginDataPB.MachineId : string.Empty;
|
||||
Logger.Info($"Sending AuthTicket for SessionId 0x{ticket.SessionId:X} to the game client on {endPointName}, machineId={machineId}");
|
||||
await HttpHelper.SendProtobufAsync(httpResponse, ticket);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a <see cref="PrecacheHeaders"/> message.
|
||||
/// </summary>
|
||||
private async Task<bool> OnPrecacheHeaders(HttpListenerRequest httpRequest, HttpListenerResponse httpResponse, MessageBuffer messageBuffer)
|
||||
{
|
||||
// The client sends this message on startup
|
||||
Logger.Trace($"Received PrecacheHeaders message");
|
||||
await HttpHelper.SendProtobufAsync(httpResponse, PrecacheHeadersMessageResponse.DefaultInstance);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
using MHServerEmu.Core.Config;
|
||||
using MHServerEmu.Core.Extensions;
|
||||
using MHServerEmu.Core.Helpers;
|
||||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Metrics;
|
||||
using MHServerEmu.Core.Network;
|
||||
using MHServerEmu.PlayerManagement;
|
||||
using MHServerEmu.PlayerManagement.Players;
|
||||
using MHServerEmu.PlayerManagement.Regions;
|
||||
|
||||
namespace MHServerEmu.Auth.Handlers
|
||||
{
|
||||
public enum AuthWebApiOutputFormat
|
||||
{
|
||||
Html,
|
||||
Json
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for web API requests sent to the <see cref="AuthServer"/>.
|
||||
/// </summary>
|
||||
public class AuthWebApiHandler
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
private static readonly bool HideSensitiveInformation = ConfigManager.Instance.GetConfig<LoggingConfig>().HideSensitiveInformation;
|
||||
|
||||
private readonly string ResponseHtml;
|
||||
private readonly string AccountCreateFormHtml;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="AuthWebApiHandler"/> instance.
|
||||
/// </summary>
|
||||
public AuthWebApiHandler()
|
||||
{
|
||||
string assetDirectory = Path.Combine(FileHelper.DataDirectory, "Auth");
|
||||
ResponseHtml = File.ReadAllText(Path.Combine(assetDirectory, "Response.html"));
|
||||
AccountCreateFormHtml = File.ReadAllText(Path.Combine(assetDirectory, "AccountCreateForm.html"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives and handles a web API request.
|
||||
/// </summary>
|
||||
public async Task HandleRequestAsync(HttpListenerRequest httpRequest, HttpListenerResponse httpResponse)
|
||||
{
|
||||
// Mask end point name if needed
|
||||
string endPointName = HideSensitiveInformation
|
||||
? httpRequest.RemoteEndPoint.ToStringMasked()
|
||||
: httpRequest.RemoteEndPoint.ToString();
|
||||
|
||||
if (Enum.TryParse(httpRequest.QueryString["outputFormat"], true, out AuthWebApiOutputFormat outputFormat) == false)
|
||||
outputFormat = AuthWebApiOutputFormat.Html;
|
||||
|
||||
// Parse query string body from POST requests
|
||||
NameValueCollection bodyQueryString = null;
|
||||
if (httpRequest.HttpMethod == "POST")
|
||||
{
|
||||
using (StreamReader reader = new(httpRequest.InputStream))
|
||||
bodyQueryString = HttpUtility.ParseQueryString(reader.ReadToEnd());
|
||||
}
|
||||
|
||||
// Handling
|
||||
switch (httpRequest.Url.LocalPath)
|
||||
{
|
||||
case "/AccountManagement/Create": await OnAccountCreate(bodyQueryString, httpResponse, outputFormat); break;
|
||||
case "/ServerStatus": await OnServerStatus(httpResponse, outputFormat); break;
|
||||
case "/RegionReport": await OnRegionReport(httpResponse, outputFormat); break;
|
||||
case "/Metrics/Performance": await OnMetricsPerformance(httpResponse, outputFormat); break;
|
||||
|
||||
default:
|
||||
Logger.Warn($"HandleRequestAsync(): Unhandled web API request\nRequest: {httpRequest.Url.LocalPath}\nRemoteEndPoint: {endPointName}\nUserAgent: {httpRequest.UserAgent}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends <see cref="ResponseData"/> as an <see cref="HttpListenerResponse"/> using the specified <see cref="AuthWebApiOutputFormat"/>.
|
||||
/// </summary>
|
||||
private async Task SendResponseAsync(ResponseData responseData, HttpListenerResponse httpResponse, AuthWebApiOutputFormat outputFormat)
|
||||
{
|
||||
if (outputFormat == AuthWebApiOutputFormat.Html)
|
||||
await HttpHelper.SendHtmlAsync(httpResponse, FormatResponseDataHtml(responseData));
|
||||
else if (outputFormat == AuthWebApiOutputFormat.Json)
|
||||
await HttpHelper.SendPlainTextAsync(httpResponse, JsonSerializer.Serialize(responseData));
|
||||
else
|
||||
Logger.Warn($"SendResponseAsync(): Unsupported output format {outputFormat}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats <see cref="ResponseData"> as an html page.
|
||||
/// </summary>
|
||||
private string FormatResponseDataHtml(ResponseData responseData)
|
||||
{
|
||||
StringBuilder sb = new(ResponseHtml);
|
||||
sb.Replace("%RESPONSE_TITLE%", responseData.Title);
|
||||
sb.Replace("%RESPONSE_TEXT%", responseData.Text);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#region Request Handling
|
||||
|
||||
/// <summary>
|
||||
/// Handles an account creation web request.
|
||||
/// </summary>
|
||||
private async Task<bool> OnAccountCreate(NameValueCollection bodyQueryString, HttpListenerResponse httpResponse, AuthWebApiOutputFormat outputFormat)
|
||||
{
|
||||
// Show account creation form when no parameters are specified in the query string
|
||||
if (bodyQueryString == null)
|
||||
{
|
||||
if (outputFormat == AuthWebApiOutputFormat.Html)
|
||||
await HttpHelper.SendHtmlAsync(httpResponse, AccountCreateFormHtml);
|
||||
else if (outputFormat == AuthWebApiOutputFormat.Json)
|
||||
await SendResponseAsync(new(false, "Invalid Request", "This request does not support JSON output."), httpResponse, outputFormat);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate input
|
||||
bool inputIsValid = true;
|
||||
inputIsValid &= string.IsNullOrWhiteSpace(bodyQueryString["email"]) == false;
|
||||
inputIsValid &= string.IsNullOrWhiteSpace(bodyQueryString["playerName"]) == false;
|
||||
inputIsValid &= string.IsNullOrWhiteSpace(bodyQueryString["password"]) == false;
|
||||
|
||||
if (inputIsValid == false)
|
||||
{
|
||||
await SendResponseAsync(new(false, "Error", "Input is not valid."), httpResponse, outputFormat);
|
||||
return false;
|
||||
}
|
||||
|
||||
(bool result, string text) = AccountManager.CreateAccount(bodyQueryString["email"].ToLower(), bodyQueryString["playerName"], bodyQueryString["password"]);
|
||||
if (HideSensitiveInformation == false) Logger.Trace(text);
|
||||
|
||||
ResponseData responseData = new(result, result ? "Success" : "Error", text);
|
||||
|
||||
await SendResponseAsync(responseData, httpResponse, outputFormat);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a server status web request.
|
||||
/// </summary>
|
||||
private async Task<bool> OnServerStatus(HttpListenerResponse httpResponse, AuthWebApiOutputFormat outputFormat)
|
||||
{
|
||||
string serverStatus = ServerManager.Instance.GetServerStatus(false);
|
||||
|
||||
// Fix line breaks for display in browsers
|
||||
if (outputFormat == AuthWebApiOutputFormat.Html)
|
||||
serverStatus = serverStatus.Replace("\n", "<br/>");
|
||||
|
||||
await SendResponseAsync(new(true, "Server Status", serverStatus), httpResponse, outputFormat);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> OnRegionReport(HttpListenerResponse httpResponse, AuthWebApiOutputFormat outputFormat)
|
||||
{
|
||||
if (ServerManager.Instance.GetGameService(GameServiceType.PlayerManager) is not PlayerManagerService playerManager)
|
||||
return false;
|
||||
|
||||
using RegionReport regionReport = new();
|
||||
playerManager.GetRegionReportData(regionReport);
|
||||
|
||||
if (outputFormat == AuthWebApiOutputFormat.Html)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
HtmlBuilder.AppendDataStructure(sb, regionReport);
|
||||
await SendResponseAsync(new(true, "Region Report", sb.ToString()), httpResponse, outputFormat);
|
||||
}
|
||||
else if (outputFormat == AuthWebApiOutputFormat.Json)
|
||||
{
|
||||
string json = JsonSerializer.Serialize(regionReport);
|
||||
await HttpHelper.SendPlainTextAsync(httpResponse, json);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> OnMetricsPerformance(HttpListenerResponse httpResponse, AuthWebApiOutputFormat outputFormat)
|
||||
{
|
||||
if (outputFormat == AuthWebApiOutputFormat.Html)
|
||||
{
|
||||
string report = MetricsManager.Instance.GeneratePerformanceReport(MetricsReportFormat.Html);
|
||||
await SendResponseAsync(new(true, "Performance Report", report), httpResponse, outputFormat);
|
||||
}
|
||||
else if (outputFormat == AuthWebApiOutputFormat.Json)
|
||||
{
|
||||
string report = MetricsManager.Instance.GeneratePerformanceReport(MetricsReportFormat.Json);
|
||||
await HttpHelper.SendPlainTextAsync(httpResponse, report);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private readonly struct ResponseData
|
||||
{
|
||||
public bool Result { get; }
|
||||
public string Title { get; }
|
||||
public string Text { get; }
|
||||
|
||||
public ResponseData(bool result, string title, string text)
|
||||
{
|
||||
Result = result;
|
||||
Title = title;
|
||||
Text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using Google.ProtocolBuffers;
|
||||
using MHServerEmu.Core.Network;
|
||||
|
||||
namespace MHServerEmu.Auth
|
||||
{
|
||||
public static class HttpHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a string as a text/plain <see cref="HttpListenerResponse"/>.
|
||||
/// </summary>
|
||||
public static async Task SendPlainTextAsync(HttpListenerResponse httpResponse, string text, int statusCode = 200)
|
||||
{
|
||||
await SendTextAsync(httpResponse, text, "text/plain", statusCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a string as a text/html <see cref="HttpListenerResponse"/>.
|
||||
/// </summary>
|
||||
public static async Task SendHtmlAsync(HttpListenerResponse httpResponse, string text, int statusCode = 200)
|
||||
{
|
||||
await SendTextAsync(httpResponse, text, "text/html", statusCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an <see cref="IMessage"/> instance as an <see cref="HttpListenerResponse"/>.
|
||||
/// </summary>
|
||||
public static async Task SendProtobufAsync(HttpListenerResponse httpResponse, IMessage message, int statusCode = 200)
|
||||
{
|
||||
MessagePackageOut messagePackage = new(message);
|
||||
|
||||
httpResponse.StatusCode = statusCode;
|
||||
httpResponse.KeepAlive = false;
|
||||
httpResponse.ContentType = "application/octet-stream";
|
||||
httpResponse.ContentLength64 = messagePackage.GetSerializedSize();
|
||||
|
||||
CodedOutputStream cos = CodedOutputStream.CreateInstance(httpResponse.OutputStream);
|
||||
await Task.Run(() => { messagePackage.WriteTo(cos); cos.Flush(); });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a string as an <see cref="HttpListenerResponse"/>.
|
||||
/// </summary>
|
||||
private static async Task SendTextAsync(HttpListenerResponse httpResponse, string text, string contentType, int statusCode = 200)
|
||||
{
|
||||
byte[] buffer = Encoding.UTF8.GetBytes(text);
|
||||
|
||||
httpResponse.StatusCode = statusCode;
|
||||
httpResponse.KeepAlive = false;
|
||||
httpResponse.ContentType = contentType;
|
||||
httpResponse.ContentLength64 = buffer.Length;
|
||||
|
||||
await httpResponse.OutputStream.WriteAsync(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Network;
|
||||
|
||||
namespace MHServerEmu.Billing
|
||||
{
|
||||
public class BillingService : IGameService
|
||||
{
|
||||
// All the message handling / order fulfillment functionality has been moved to Games.MTXStore.CatalogManager.
|
||||
// I'm keeping this service stub because eventually it will be used for converting ES to G via the store interface.
|
||||
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
public GameServiceState State { get; private set; } = GameServiceState.Created;
|
||||
|
||||
public BillingService() { }
|
||||
|
||||
#region IGameService Implementation
|
||||
|
||||
public void Run()
|
||||
{
|
||||
State = GameServiceState.Running;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
State = GameServiceState.Shutdown;
|
||||
}
|
||||
|
||||
public void ReceiveServiceMessage<T>(in T message) where T : struct, IGameServiceMessage
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
default:
|
||||
Logger.Warn($"ReceiveServiceMessage(): Unhandled service message type {typeof(T).Name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetStatus()
|
||||
{
|
||||
return "Running";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyVersion>0.7.0.0</AssemblyVersion>
|
||||
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MHServerEmu.Core\MHServerEmu.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Google.ProtocolBuffers">
|
||||
<HintPath>..\..\dep\protobuf-csharp\Google.ProtocolBuffers.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
100
src/MHServerEmu.Core/Collections/FixedDeque.cs
Normal file
100
src/MHServerEmu.Core/Collections/FixedDeque.cs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
namespace MHServerEmu.Core.Collections
|
||||
{
|
||||
public class FixedDeque<T>
|
||||
{
|
||||
private readonly T[] _items;
|
||||
private int _first;
|
||||
private int _last;
|
||||
private readonly int _maxSize;
|
||||
|
||||
public FixedDeque(int size)
|
||||
{
|
||||
_maxSize = size + 1;
|
||||
_items = new T[_maxSize];
|
||||
_first = 0;
|
||||
_last = 0;
|
||||
}
|
||||
|
||||
public int Capacity => _maxSize - 1;
|
||||
public int Size => (_last - _first + _maxSize) % _maxSize;
|
||||
public bool Empty => _first == _last;
|
||||
public void Clear()
|
||||
{
|
||||
Array.Clear(_items, 0, _items.Length);
|
||||
_first = _last = 0;
|
||||
}
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index >= Size)
|
||||
throw new IndexOutOfRangeException();
|
||||
return _items[(_first + index) % _maxSize];
|
||||
}
|
||||
set
|
||||
{
|
||||
if (index >= Size)
|
||||
throw new IndexOutOfRangeException();
|
||||
_items[(_first + index) % _maxSize] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public T Front => _items[_first];
|
||||
public T Back => _items[(_last + _maxSize - 1) % _maxSize];
|
||||
|
||||
public void PushBack(T value)
|
||||
{
|
||||
_items[_last] = value;
|
||||
_last = (_last + 1) % _maxSize;
|
||||
}
|
||||
|
||||
public void PushFront(T value)
|
||||
{
|
||||
_first = (_first + _maxSize - 1) % _maxSize;
|
||||
_items[_first] = value;
|
||||
}
|
||||
|
||||
public T PopBack()
|
||||
{
|
||||
_last = (_last + _maxSize - 1) % _maxSize;
|
||||
T result = _items[_last];
|
||||
_items[_last] = default;
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TryPopBack(out T value)
|
||||
{
|
||||
if (Empty)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
_last = (_last + _maxSize - 1) % _maxSize;
|
||||
value = _items[_last];
|
||||
_items[_last] = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
public T PopFront()
|
||||
{
|
||||
T result = _items[_first];
|
||||
_items[_first] = default;
|
||||
_first = (_first + 1) % _maxSize;
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TryPopFront(out T value)
|
||||
{
|
||||
if (Empty)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
value = _items[_first];
|
||||
_items[_first] = default;
|
||||
_first = (_first + 1) % _maxSize;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
namespace MHServerEmu.Core.Collections
|
||||
{
|
||||
public class FixedPriorityQueue<T> where T : IComparable<T>
|
||||
public readonly struct FixedPriorityQueue<T> where T : IComparable<T>
|
||||
{
|
||||
// Our implementation is just a List<T> wrapper with no data of its own, so we can get away with making it a readonly struct.
|
||||
// If this ever needs to stop being the case, turn it into a fully featured ICollection<T> implementation and pool it.
|
||||
|
||||
private readonly List<T> _items;
|
||||
|
||||
public bool Empty => _items.Count == 0;
|
||||
public int Count => _items.Count;
|
||||
public T Top => _items[0];
|
||||
|
|
@ -12,6 +16,11 @@
|
|||
_items = new(capacity);
|
||||
}
|
||||
|
||||
public FixedPriorityQueue(List<T> items)
|
||||
{
|
||||
_items = items;
|
||||
}
|
||||
|
||||
public void Push(T value)
|
||||
{
|
||||
_items.Add(value);
|
||||
|
|
@ -27,7 +36,10 @@
|
|||
_items.RemoveAt(_items.Count - 1);
|
||||
}
|
||||
|
||||
public void Clear() => _items.Clear();
|
||||
public void Clear()
|
||||
{
|
||||
_items.Clear();
|
||||
}
|
||||
|
||||
public void Heapify()
|
||||
{
|
||||
|
|
@ -104,102 +116,4 @@
|
|||
PushHeapIndex(list, holeIndex, topIndex, value);
|
||||
}
|
||||
}
|
||||
|
||||
public class FixedDeque<T>
|
||||
{
|
||||
private readonly T[] _items;
|
||||
private int _first;
|
||||
private int _last;
|
||||
private readonly int _maxSize;
|
||||
|
||||
public FixedDeque(int size)
|
||||
{
|
||||
_maxSize = size + 1;
|
||||
_items = new T[_maxSize];
|
||||
_first = 0;
|
||||
_last = 0;
|
||||
}
|
||||
|
||||
public int Capacity => _maxSize - 1;
|
||||
public int Size => (_last - _first + _maxSize) % _maxSize;
|
||||
public bool Empty => _first == _last;
|
||||
public void Clear()
|
||||
{
|
||||
Array.Clear(_items, 0, _items.Length);
|
||||
_first = _last = 0;
|
||||
}
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index >= Size)
|
||||
throw new IndexOutOfRangeException();
|
||||
return _items[(_first + index) % _maxSize];
|
||||
}
|
||||
set
|
||||
{
|
||||
if (index >= Size)
|
||||
throw new IndexOutOfRangeException();
|
||||
_items[(_first + index) % _maxSize] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public T Front => _items[_first];
|
||||
public T Back => _items[(_last + _maxSize - 1) % _maxSize];
|
||||
|
||||
public void PushBack(T value)
|
||||
{
|
||||
_items[_last] = value;
|
||||
_last = (_last + 1) % _maxSize;
|
||||
}
|
||||
|
||||
public void PushFront(T value)
|
||||
{
|
||||
_first = (_first + _maxSize - 1) % _maxSize;
|
||||
_items[_first] = value;
|
||||
}
|
||||
|
||||
public T PopBack()
|
||||
{
|
||||
_last = (_last + _maxSize - 1) % _maxSize;
|
||||
T result = _items[_last];
|
||||
_items[_last] = default;
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TryPopBack(out T value)
|
||||
{
|
||||
if (Empty)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
_last = (_last + _maxSize - 1) % _maxSize;
|
||||
value = _items[_last];
|
||||
_items[_last] = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
public T PopFront()
|
||||
{
|
||||
T result = _items[_first];
|
||||
_items[_first] = default;
|
||||
_first = (_first + 1) % _maxSize;
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TryPopFront(out T value)
|
||||
{
|
||||
if (Empty)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
value = _items[_first];
|
||||
_items[_first] = default;
|
||||
_first = (_first + 1) % _maxSize;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/MHServerEmu.Core/Collections/InlineArray.cs
Normal file
49
src/MHServerEmu.Core/Collections/InlineArray.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MHServerEmu.Core.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic inline array of 2 elements of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
[InlineArray(2)]
|
||||
public struct InlineArray2<T>
|
||||
{
|
||||
private T _element0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic inline array of 3 elements of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
[InlineArray(3)]
|
||||
public struct InlineArray3<T>
|
||||
{
|
||||
private T _element0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic inline array of 4 elements of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
[InlineArray(4)]
|
||||
public struct InlineArray4<T>
|
||||
{
|
||||
private T _element0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic inline array of 6 elements of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
[InlineArray(6)]
|
||||
public struct InlineArray6<T>
|
||||
{
|
||||
private T _element0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic inline array of 8 elements of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
[InlineArray(8)]
|
||||
public struct InlineArray8<T>
|
||||
{
|
||||
private T _element0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +1,59 @@
|
|||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MHServerEmu.Core.Collections
|
||||
{
|
||||
public class InvasiveList<T>
|
||||
{
|
||||
private readonly Iterator[] _iterators;
|
||||
|
||||
private readonly Stack<Iterator> _iteratorPool;
|
||||
private Iterator _reusableIterator;
|
||||
|
||||
private int _numIterators;
|
||||
|
||||
public int Id { get; private set; }
|
||||
public T Head { get; set; }
|
||||
public T Head { get; private set; }
|
||||
public T Tail { get; private set; }
|
||||
public int Count { get; private set; }
|
||||
|
||||
private Iterator[] _iterators;
|
||||
private int _numIterators;
|
||||
private int _maxIterators;
|
||||
public bool IsEmpty { get => Head == null; }
|
||||
|
||||
public InvasiveList(int maxIterators)
|
||||
public InvasiveList(int maxIterators, int id = 0)
|
||||
{
|
||||
_maxIterators = maxIterators;
|
||||
_iterators = new Iterator[_maxIterators];
|
||||
}
|
||||
_iterators = new Iterator[maxIterators];
|
||||
|
||||
if (maxIterators > 1)
|
||||
_iteratorPool = new();
|
||||
|
||||
public InvasiveList(int maxIterators, int id)
|
||||
{
|
||||
_maxIterators = maxIterators;
|
||||
_iterators = new Iterator[_maxIterators];
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public IEnumerable<T> Iterate()
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
var iterator = new Iterator(this);
|
||||
Iterator iterator;
|
||||
|
||||
try
|
||||
if (_iteratorPool != null)
|
||||
{
|
||||
while (iterator.End() == false)
|
||||
{
|
||||
var element = iterator.Current;
|
||||
iterator.MoveNext();
|
||||
yield return element;
|
||||
}
|
||||
if (_iteratorPool.TryPop(out iterator) == false)
|
||||
iterator = new(this);
|
||||
}
|
||||
finally
|
||||
else
|
||||
{
|
||||
UnregisterIterator(iterator);
|
||||
_reusableIterator ??= new(this);
|
||||
iterator = _reusableIterator;
|
||||
}
|
||||
|
||||
iterator.Initialize();
|
||||
return iterator;
|
||||
}
|
||||
|
||||
public bool IsEmpty() => Head == null;
|
||||
|
||||
public void Remove(T element)
|
||||
{
|
||||
if (element == null || Contains(element) == false) return;
|
||||
|
||||
var node = GetInvasiveListNode(element, Id);
|
||||
if (node == null) return;
|
||||
ref var node = ref GetInvasiveListNode(element, Id);
|
||||
if (Unsafe.IsNullRef(ref node)) return;
|
||||
|
||||
for (int i = 0; i < _numIterators; i++)
|
||||
{
|
||||
|
|
@ -68,16 +69,16 @@ namespace MHServerEmu.Core.Collections
|
|||
if (node.Next != null)
|
||||
{
|
||||
T nextElement = node.Next;
|
||||
var nextNode = GetInvasiveListNode(nextElement, Id);
|
||||
if (nextNode != null)
|
||||
ref var nextNode = ref GetInvasiveListNode(nextElement, Id);
|
||||
if (Unsafe.IsNullRef(ref nextNode) == false)
|
||||
nextNode.Prev = node.Prev;
|
||||
}
|
||||
|
||||
if (node.Prev != null)
|
||||
{
|
||||
T prevElement = node.Prev;
|
||||
var prevNode = GetInvasiveListNode(prevElement, Id);
|
||||
if (prevNode != null)
|
||||
ref var prevNode = ref GetInvasiveListNode(prevElement, Id);
|
||||
if (Unsafe.IsNullRef(ref prevNode) == false)
|
||||
prevNode.Next = node.Next;
|
||||
}
|
||||
|
||||
|
|
@ -93,11 +94,11 @@ namespace MHServerEmu.Core.Collections
|
|||
if (oldElement == null || Contains(oldElement) == false) return;
|
||||
if (element == null || Contains(element)) return;
|
||||
|
||||
var node = GetInvasiveListNode(element, Id);
|
||||
if (node == null) return;
|
||||
ref var node = ref GetInvasiveListNode(element, Id);
|
||||
if (Unsafe.IsNullRef(ref node)) return;
|
||||
|
||||
var oldNode = GetInvasiveListNode(oldElement, Id);
|
||||
if (oldNode == null) return;
|
||||
ref var oldNode = ref GetInvasiveListNode(oldElement, Id);
|
||||
if (Unsafe.IsNullRef(ref oldNode)) return;
|
||||
|
||||
var oldPrev = oldNode.Prev;
|
||||
oldNode.Prev = element;
|
||||
|
|
@ -106,8 +107,8 @@ namespace MHServerEmu.Core.Collections
|
|||
|
||||
if (oldPrev != null)
|
||||
{
|
||||
var oldPrevNode = GetInvasiveListNode(oldPrev, Id);
|
||||
if (oldPrevNode == null) return;
|
||||
ref var oldPrevNode = ref GetInvasiveListNode(oldPrev, Id);
|
||||
if (Unsafe.IsNullRef(ref oldPrevNode)) return;
|
||||
oldPrevNode.Next = element;
|
||||
}
|
||||
else
|
||||
|
|
@ -120,14 +121,14 @@ namespace MHServerEmu.Core.Collections
|
|||
{
|
||||
if (element == null || Contains(element)) return;
|
||||
|
||||
var node = GetInvasiveListNode(element, Id);
|
||||
if (node == null) return;
|
||||
ref var node = ref GetInvasiveListNode(element, Id);
|
||||
if (Unsafe.IsNullRef(ref node)) return;
|
||||
|
||||
node.Prev = Tail;
|
||||
if (Tail != null)
|
||||
{
|
||||
var tailNode = GetInvasiveListNode(Tail, Id);
|
||||
if (tailNode == null) return;
|
||||
ref var tailNode = ref GetInvasiveListNode(Tail, Id);
|
||||
if (Unsafe.IsNullRef(ref tailNode)) return;
|
||||
tailNode.Next = element;
|
||||
}
|
||||
else
|
||||
|
|
@ -137,20 +138,23 @@ namespace MHServerEmu.Core.Collections
|
|||
Count++;
|
||||
}
|
||||
|
||||
public virtual InvasiveListNode<T> GetInvasiveListNode(T element, int listId) => null;
|
||||
public virtual ref InvasiveListNode<T> GetInvasiveListNode(T element, int listId)
|
||||
{
|
||||
return ref Unsafe.NullRef<InvasiveListNode<T>>();
|
||||
}
|
||||
|
||||
public bool Contains(T element)
|
||||
{
|
||||
if (element == null) return false;
|
||||
var node = GetInvasiveListNode(element, Id);
|
||||
if (node == null) return false;
|
||||
ref var node = ref GetInvasiveListNode(element, Id);
|
||||
if (Unsafe.IsNullRef(ref node)) return false;
|
||||
return node.Next != null || node.Prev != null || element.Equals(Head);
|
||||
}
|
||||
|
||||
private void RegisterIterator(Iterator iterator)
|
||||
{
|
||||
if (_numIterators >= _maxIterators)
|
||||
throw new InvalidOperationException($"Too many iterators '{_maxIterators}' for invasive list");
|
||||
if (_numIterators >= _iterators.Length)
|
||||
throw new InvalidOperationException($"Too many iterators '{_iterators.Length}' for invasive list");
|
||||
|
||||
_iterators[_numIterators++] = iterator;
|
||||
}
|
||||
|
|
@ -165,71 +169,75 @@ namespace MHServerEmu.Core.Collections
|
|||
|
||||
_iterators[_numIterators - 1] = null;
|
||||
_numIterators--;
|
||||
|
||||
// pool iterator instance for reuse
|
||||
iterator.Reset();
|
||||
_iteratorPool?.Push(iterator);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Iterator not found in iterator collection of invasive list!");
|
||||
}
|
||||
|
||||
public class Iterator : IEnumerator<T>
|
||||
public sealed class Iterator : IEnumerator<T>
|
||||
{
|
||||
private InvasiveList<T> _list;
|
||||
public bool SkipNext { get; set; }
|
||||
private readonly InvasiveList<T> _list;
|
||||
|
||||
private bool _start = true;
|
||||
|
||||
public T Current { get; private set; }
|
||||
object IEnumerator.Current { get => Current; }
|
||||
|
||||
public bool SkipNext { get; set; } = false;
|
||||
|
||||
public Iterator(InvasiveList<T> invasiveList)
|
||||
{
|
||||
_list = invasiveList;
|
||||
Current = _list.Head;
|
||||
SkipNext = false;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_list.RegisterIterator(this);
|
||||
}
|
||||
|
||||
public T Current { get; private set; }
|
||||
object IEnumerator.Current => Current;
|
||||
public void Dispose() { }
|
||||
public void Reset() { }
|
||||
public void Dispose()
|
||||
{
|
||||
_list.UnregisterIterator(this);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_start = true;
|
||||
Current = default;
|
||||
SkipNext = false;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (SkipNext) SkipNext = false;
|
||||
else if (Current != null)
|
||||
Current = _list.GetInvasiveListNode(Current, _list.Id).Next;
|
||||
if (_start)
|
||||
{
|
||||
Current = _list.Head;
|
||||
_start = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SkipNext)
|
||||
SkipNext = false;
|
||||
else if (Current != null)
|
||||
Current = _list.GetInvasiveListNode(Current, _list.Id).Next;
|
||||
}
|
||||
|
||||
return true;
|
||||
return Current != null;
|
||||
}
|
||||
|
||||
public bool End() => Current == null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class InvasiveListNode<T>
|
||||
public struct InvasiveListNode<T>
|
||||
{
|
||||
public T Next { get; set; }
|
||||
public T Prev { get; set; }
|
||||
public T Next;
|
||||
public T Prev;
|
||||
|
||||
public void Clear() => Next = Prev = default;
|
||||
}
|
||||
|
||||
public class InvasiveListNodeCollection<T>
|
||||
{
|
||||
private readonly InvasiveListNode<T>[] _nodes;
|
||||
private int _numLists;
|
||||
|
||||
public InvasiveListNodeCollection(int numLists)
|
||||
{
|
||||
_numLists = numLists;
|
||||
_nodes = new InvasiveListNode<T>[_numLists];
|
||||
for (int i = 0; i < _numLists; i++)
|
||||
_nodes[i] = new();
|
||||
}
|
||||
|
||||
public InvasiveListNode<T> GetInvasiveListNode(int listIndex)
|
||||
{
|
||||
if (listIndex >= 0 && listIndex < _numLists)
|
||||
return _nodes[listIndex];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,9 +33,7 @@ namespace MHServerEmu.Core.Collections
|
|||
|
||||
public Picker(Picker<T> other)
|
||||
{
|
||||
_elements = new(other._elements.Count);
|
||||
foreach (WeightedElement element in other._elements)
|
||||
_elements.Add(element);
|
||||
_elements = new(other._elements);
|
||||
|
||||
// IMPORTANT: The copy needs to use the same instance of random as the original to preserve the RNG sequence,
|
||||
// otherwise PickValidItem() and PickWeightTryAll() will keep picking the same things.
|
||||
|
|
@ -188,15 +186,10 @@ namespace MHServerEmu.Core.Collections
|
|||
_weights = 0;
|
||||
}
|
||||
|
||||
private class WeightedElement
|
||||
private readonly struct WeightedElement(T element, int weight)
|
||||
{
|
||||
public T Element { get; }
|
||||
public int Weight { get; }
|
||||
public WeightedElement(T element, int weight)
|
||||
{
|
||||
Element = element;
|
||||
Weight = weight;
|
||||
}
|
||||
public T Element { get; } = element;
|
||||
public int Weight { get; } = weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
src/MHServerEmu.Core/Collections/PoolableStack.cs
Normal file
22
src/MHServerEmu.Core/Collections/PoolableStack.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace MHServerEmu.Core.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// A modification of <see cref="Stack{T}"/> that implements <see cref="ICollection{T}"/> for compatibility with our CollectionPool implementation.
|
||||
/// </summary>
|
||||
public sealed class PoolableStack<T> : Stack<T>, ICollection<T>
|
||||
{
|
||||
public bool IsReadOnly { get => false; }
|
||||
|
||||
[Obsolete("This method is not supported.")]
|
||||
public void Add(T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
[Obsolete("This method is not supported.")]
|
||||
public bool Remove(T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,15 +70,12 @@ namespace MHServerEmu.Core.Collisions
|
|||
/// <summary>
|
||||
/// Return the coordinates of the corners
|
||||
/// </summary>
|
||||
public Point2[] GetPoints()
|
||||
public void GetPoints(Span<Point2> points)
|
||||
{
|
||||
return new Point2[]
|
||||
{
|
||||
new (Min.X, Min.Y),
|
||||
new (Min.X, Max.Y),
|
||||
new (Max.X, Max.Y),
|
||||
new (Max.X, Min.Y)
|
||||
};
|
||||
points[0] = new(Min.X, Min.Y);
|
||||
points[1] = new(Min.X, Max.Y);
|
||||
points[2] = new(Max.X, Max.Y);
|
||||
points[3] = new(Max.X, Min.Y);
|
||||
}
|
||||
|
||||
public Aabb2 Translate(Vector2 newPosition) => new(Min + newPosition, Max + newPosition);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
using System.Text;
|
||||
using MHServerEmu.Core.Collections;
|
||||
using MHServerEmu.Core.VectorMath;
|
||||
|
||||
namespace MHServerEmu.Core.Collisions
|
||||
{
|
||||
public struct Triangle
|
||||
{
|
||||
public Vector3[] Points;
|
||||
public InlineArray3<Vector3> Points;
|
||||
|
||||
public Triangle(Vector3 p0, Vector3 p1, Vector3 p2)
|
||||
{
|
||||
Points = new Vector3[3];
|
||||
Points[0] = p0;
|
||||
Points[1] = p1;
|
||||
Points[2] = p2;
|
||||
|
|
|
|||
|
|
@ -106,6 +106,17 @@ namespace MHServerEmu.Core.Extensions
|
|||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static bool IsAscii(this ReadOnlySpan<char> chars)
|
||||
{
|
||||
foreach (char c in chars)
|
||||
{
|
||||
if (char.IsAscii(c) == false)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
src/MHServerEmu.Core/Extensions/DictionaryExtensions.cs
Normal file
44
src/MHServerEmu.Core/Extensions/DictionaryExtensions.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MHServerEmu.Core.Extensions
|
||||
{
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a <see langword="ref"/> to a value corresponding to a <typeparamref name="TKey"/> key in this <see cref="Dictionary{TKey, TValue}"/>.
|
||||
/// Returns <see langword="true"/> if a value was found.
|
||||
/// </summary>
|
||||
public static bool TryGetValueRef<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, ref TValue value)
|
||||
{
|
||||
value = ref CollectionsMarshal.GetValueRefOrNullRef(dictionary, key);
|
||||
return Unsafe.IsNullRef(ref value) == false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see langword="ref"/> to a value corresponding to a <typeparamref name="TKey"/> key in this <see cref="Dictionary{TKey, TValue}"/>.
|
||||
/// Adds a <see langword="default"/> value if no existing value corresponds to the specified key.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This has behavior similar to std::map indexers in C++.
|
||||
/// </remarks>
|
||||
public static ref TValue GetValueRefOrAddDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key)
|
||||
{
|
||||
return ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see langword="ref"/> to a value corresponding to a <typeparamref name="TKey"/> key in this <see cref="Dictionary{TKey, TValue}"/>.
|
||||
/// Adds a <see langword="default"/> value if no existing value corresponds to the specified key.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This has behavior similar to std::map indexers in C++.
|
||||
/// </remarks>
|
||||
public static ref TValue GetValueRefOrAddDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, out bool added)
|
||||
{
|
||||
ref TValue value = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out bool exists);
|
||||
added = !exists;
|
||||
return ref value;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/MHServerEmu.Core/Extensions/HashSetExtensions.cs
Normal file
24
src/MHServerEmu.Core/Extensions/HashSetExtensions.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
namespace MHServerEmu.Core.Extensions
|
||||
{
|
||||
public static class HashSetExtensions
|
||||
{
|
||||
public static void Set<T>(this HashSet<T> hashSet, HashSet<T> other)
|
||||
{
|
||||
hashSet.Clear();
|
||||
foreach (T item in other)
|
||||
hashSet.Add(item);
|
||||
}
|
||||
|
||||
public static void Insert<T>(this HashSet<T> hashSet, HashSet<T> other)
|
||||
{
|
||||
foreach (T item in other)
|
||||
hashSet.Add(item);
|
||||
}
|
||||
|
||||
public static void Insert<T>(this HashSet<T> hashSet, T[] other)
|
||||
{
|
||||
foreach (T item in other)
|
||||
hashSet.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/MHServerEmu.Core/Extensions/ListExtensions.cs
Normal file
37
src/MHServerEmu.Core/Extensions/ListExtensions.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MHServerEmu.Core.Extensions
|
||||
{
|
||||
public static class ListExtensions
|
||||
{
|
||||
public static void Set<T>(this List<T> list, IEnumerable<T> other)
|
||||
{
|
||||
list.Clear();
|
||||
list.AddRange(other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the provided <see langword="struct"/> <typeparamref name="T"/> to this <see cref="List{T}"/> the specified number of times.
|
||||
/// </summary>
|
||||
public static void Fill<T>(this List<T> list, T value, int count) where T: struct
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
list.Add(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a range from another <see cref="List{T}"/> instance avoiding intermediary heap allocations.
|
||||
/// </summary>
|
||||
public static void AddRange<T>(this List<T> list, List<T> other, int start, int count)
|
||||
{
|
||||
Span<T> items = CollectionsMarshal.AsSpan(other);
|
||||
items = items.Slice(start, count);
|
||||
list.AddRange(items);
|
||||
}
|
||||
|
||||
public static Span<T> AsSpan<T>(this List<T> list)
|
||||
{
|
||||
return CollectionsMarshal.AsSpan(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
using System.Net;
|
||||
|
||||
namespace MHServerEmu.Core.Extensions
|
||||
{
|
||||
public static class MiscExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a masked <see cref="string"/> representation of this <see cref="IPEndPoint"/>.
|
||||
/// </summary>
|
||||
/// <param name="endpoint"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToStringMasked(this IPEndPoint endpoint)
|
||||
{
|
||||
string address = endpoint.Address.ToString();
|
||||
return $"{address.Substring(0, address.Length / 2)}****:{endpoint.Port}";
|
||||
}
|
||||
|
||||
public static void Set<T>(this List<T> list, IEnumerable<T> other)
|
||||
{
|
||||
list.Clear();
|
||||
list.AddRange(other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the provided <see langword="struct"/> <typeparamref name="T"/> to this <see cref="List{T}"/> the specified number of times.
|
||||
/// </summary>
|
||||
public static void Fill<T>(this List<T> list, T value, int count) where T: struct
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
list.Add(value);
|
||||
}
|
||||
|
||||
public static void Set<T>(this HashSet<T> hashSet, HashSet<T> other)
|
||||
{
|
||||
hashSet.Clear();
|
||||
foreach (T item in other)
|
||||
hashSet.Add(item);
|
||||
}
|
||||
|
||||
public static void Insert<T>(this HashSet<T> hashSet, HashSet<T> other)
|
||||
{
|
||||
foreach (T item in other)
|
||||
hashSet.Add(item);
|
||||
}
|
||||
|
||||
public static void Insert<T>(this HashSet<T> hashSet, T[] other)
|
||||
{
|
||||
foreach (T item in other)
|
||||
hashSet.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,14 +66,13 @@ namespace MHServerEmu.Core.Extensions
|
|||
if (count == 0)
|
||||
return 0f;
|
||||
|
||||
List<float> list = ListPool<float>.Instance.Get(count);
|
||||
using var listHandle = ListPool<float>.Instance.Get(count, out List<float> list);
|
||||
foreach (float value in values)
|
||||
list.Add(value);
|
||||
|
||||
list.Sort();
|
||||
float median = list[count / 2];
|
||||
|
||||
ListPool<float>.Instance.Return(list);
|
||||
return median;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/MHServerEmu.Core/Extensions/WebExtensions.cs
Normal file
27
src/MHServerEmu.Core/Extensions/WebExtensions.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using MHServerEmu.Core.Config;
|
||||
using MHServerEmu.Core.Helpers;
|
||||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Network.Web;
|
||||
|
||||
namespace MHServerEmu.Core.Extensions
|
||||
{
|
||||
public static class WebExtensions
|
||||
{
|
||||
private static readonly bool HideSensitiveInformation = ConfigManager.Instance.GetConfig<LoggingConfig>().HideSensitiveInformation;
|
||||
|
||||
// These are extensions rather thanWebRequestContext methods to keep the latter free
|
||||
// from MHServerEmu specific dependencies.
|
||||
|
||||
public static string GetIPAddressHandle(this WebRequestContext context, out string ipAddress)
|
||||
{
|
||||
ipAddress = context.GetIPAddress();
|
||||
// Hash the IP address to prevent it from appearing in logs if needed
|
||||
return HideSensitiveInformation ? $"0x{HashHelper.Djb2(ipAddress):X8}" : ipAddress;
|
||||
}
|
||||
|
||||
public static string GetIPAddressHandle(this WebRequestContext context)
|
||||
{
|
||||
return context.GetIPAddressHandle(out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,9 +57,8 @@ namespace MHServerEmu.Core.Helpers
|
|||
{
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
T data = JsonSerializer.Deserialize<T>(json, options);
|
||||
return data;
|
||||
using FileStream fs = File.OpenRead(path);
|
||||
return JsonSerializer.Deserialize<T>(fs, options);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -73,11 +72,11 @@ namespace MHServerEmu.Core.Helpers
|
|||
public static void SerializeJson<T>(string path, T @object, JsonSerializerOptions options = null)
|
||||
{
|
||||
string dirName = Path.GetDirectoryName(path);
|
||||
if (Directory.Exists(dirName) == false)
|
||||
if (string.IsNullOrWhiteSpace(dirName) == false && Directory.Exists(dirName) == false)
|
||||
Directory.CreateDirectory(dirName);
|
||||
|
||||
string json = JsonSerializer.Serialize(@object, options);
|
||||
File.WriteAllText(path, json);
|
||||
using FileStream fs = File.Create(path);
|
||||
JsonSerializer.Serialize(fs, @object, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -34,15 +34,23 @@ namespace MHServerEmu.Core.Helpers
|
|||
return zlib.crc32(0, bytes, bytes.Length);
|
||||
}
|
||||
|
||||
public static uint Djb2(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
uint hash = 5381;
|
||||
foreach (byte b in bytes)
|
||||
hash = (hash << 5) + hash + b;
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hashes a <see cref="string"/> using the djb2 algorithm.
|
||||
/// </summary>
|
||||
public static uint Djb2(string str)
|
||||
{
|
||||
uint hash = 5381;
|
||||
for (int i = 0; i < str.Length; i++)
|
||||
hash = (hash << 5) + hash + ((byte)str[i]);
|
||||
return hash;
|
||||
int numBytes = Encoding.UTF8.GetByteCount(str);
|
||||
Span<byte> bytes = stackalloc byte[numBytes];
|
||||
Encoding.UTF8.GetBytes(str, bytes);
|
||||
return Djb2(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,131 +0,0 @@
|
|||
using System.Text;
|
||||
|
||||
namespace MHServerEmu.Core.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for data structures that can be represented in HTML using <see cref="HtmlBuilder"/>.
|
||||
/// </summary>
|
||||
public interface IHtmlDataStructure
|
||||
{
|
||||
public void BuildHtml(StringBuilder sb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper functions to build HTML using <see cref="StringBuilder"/>.
|
||||
/// </summary>
|
||||
public static class HtmlBuilder
|
||||
{
|
||||
#region Headers
|
||||
|
||||
public static void AppendHeader1(StringBuilder sb, string text)
|
||||
{
|
||||
AppendDataLine(sb, "h1", text);
|
||||
}
|
||||
|
||||
public static void AppendHeader2(StringBuilder sb, string text)
|
||||
{
|
||||
AppendDataLine(sb, "h2", text);
|
||||
}
|
||||
|
||||
public static void AppendHeader3(StringBuilder sb, string text)
|
||||
{
|
||||
AppendDataLine(sb, "h3", text);
|
||||
}
|
||||
|
||||
public static void AppendHeader4(StringBuilder sb, string text)
|
||||
{
|
||||
AppendDataLine(sb, "h4", text);
|
||||
}
|
||||
|
||||
public static void AppendHeader5(StringBuilder sb, string text)
|
||||
{
|
||||
AppendDataLine(sb, "h5", text);
|
||||
}
|
||||
|
||||
public static void AppendHeader6(StringBuilder sb, string text)
|
||||
{
|
||||
AppendDataLine(sb, "h6", text);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Text
|
||||
|
||||
public static void AppendParagraph(StringBuilder sb, string text)
|
||||
{
|
||||
AppendDataLine(sb, "p", text);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lists
|
||||
|
||||
public static void BeginUnorderedList(StringBuilder sb)
|
||||
{
|
||||
sb.AppendLine("<ul>");
|
||||
}
|
||||
|
||||
public static void EndUnorderedList(StringBuilder sb)
|
||||
{
|
||||
sb.AppendLine("</ul>");
|
||||
}
|
||||
|
||||
public static void AppendListItem(StringBuilder sb, string item)
|
||||
{
|
||||
AppendDataLine(sb, "li", item);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tables
|
||||
|
||||
public static void BeginTable(StringBuilder sb)
|
||||
{
|
||||
sb.AppendLine("<table>");
|
||||
}
|
||||
|
||||
public static void EndTable(StringBuilder sb)
|
||||
{
|
||||
sb.AppendLine("</table>");
|
||||
}
|
||||
|
||||
public static void AppendTableRow(StringBuilder sb, params object[] data)
|
||||
{
|
||||
sb.Append("<tr>");
|
||||
foreach (object dataIt in data)
|
||||
AppendTableRowData(sb, dataIt);
|
||||
sb.AppendLine("</tr>");
|
||||
}
|
||||
|
||||
private static void AppendTableRowData(StringBuilder sb, object data)
|
||||
{
|
||||
AppendData(sb, "td", data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Custom Data Structures
|
||||
|
||||
public static void AppendDataStructure<T>(StringBuilder sb, in T htmlBuilder) where T: IHtmlDataStructure
|
||||
{
|
||||
htmlBuilder.BuildHtml(sb);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Common
|
||||
|
||||
private static void AppendData(StringBuilder sb, string tag, object data)
|
||||
{
|
||||
sb.AppendFormat("<{0}>{1}</{0}>", tag, data);
|
||||
}
|
||||
|
||||
private static void AppendDataLine(StringBuilder sb, string tag, object data)
|
||||
{
|
||||
sb.AppendFormat("<{0}>{1}</{0}>", tag, data);
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
using System.Text;
|
||||
|
||||
namespace MHServerEmu.Core.Logging
|
||||
namespace MHServerEmu.Core.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// A timestamped log message.
|
||||
|
|
@ -9,8 +7,6 @@ namespace MHServerEmu.Core.Logging
|
|||
{
|
||||
private const string TimeFormat = "yyyy.MM.dd HH:mm:ss.fff";
|
||||
|
||||
private static readonly StringBuilder StringBuilder = new();
|
||||
|
||||
public DateTime Timestamp { get; }
|
||||
public LoggingLevel Level { get; }
|
||||
public string Logger { get; }
|
||||
|
|
@ -36,26 +32,45 @@ namespace MHServerEmu.Core.Logging
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{Timestamp.ToString(TimeFormat)}] [{Level,5}] [{Logger}] {Message}";
|
||||
Span<char> formattedTimestamp = stackalloc char[TimeFormat.Length];
|
||||
Timestamp.TryFormat(formattedTimestamp, out _, TimeFormat);
|
||||
return $"[{formattedTimestamp}] [{Level,5}] [{Logger}] {Message}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that represents this <see cref="LogMessage"/> with or without a timestamp.
|
||||
/// </summary>
|
||||
public string ToString(bool includeTimestamps)
|
||||
public string ToString(bool includeTimestamp)
|
||||
{
|
||||
lock (StringBuilder) // This shouldn't be called from multiple threads unless in synchronous mode
|
||||
if (includeTimestamp)
|
||||
return ToString();
|
||||
|
||||
return $"[{Level,5}] [{Logger}] {Message}";
|
||||
}
|
||||
|
||||
public void WriteTo(TextWriter writer, bool includeTimestamp, bool writeLine)
|
||||
{
|
||||
// TextWriter.Write() with format arguments causes less memory allocation than string interpolation as of .NET 8.
|
||||
// We can't use this for our timestamp though because Span cannot be cast to an object.
|
||||
|
||||
// We can potentially make use of interpolated strings here without sacrificing performance with a custom InterpolatedStringHandler implementation.
|
||||
// https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/interpolated-string-handler
|
||||
|
||||
if (includeTimestamp)
|
||||
{
|
||||
if (includeTimestamps)
|
||||
StringBuilder.Append($"[{Timestamp.ToString(TimeFormat)}] ");
|
||||
|
||||
StringBuilder.Append($"[{Level,5}] [{Logger}] {Message}");
|
||||
|
||||
string str = StringBuilder.ToString();
|
||||
StringBuilder.Clear();
|
||||
|
||||
return str;
|
||||
Span<char> formattedTimestamp = stackalloc char[TimeFormat.Length];
|
||||
Timestamp.TryFormat(formattedTimestamp, out _, TimeFormat);
|
||||
writer.Write('[');
|
||||
writer.Write(formattedTimestamp);
|
||||
writer.Write(']');
|
||||
writer.Write(' ');
|
||||
}
|
||||
|
||||
// Enum.GetName() doesn't allocate memory unlike ToString().
|
||||
writer.Write("[{0,5}] [{1}] {2}", Enum.GetName(Level), Logger, Message);
|
||||
|
||||
if (writeLine)
|
||||
writer.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using MHServerEmu.Core.Config;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MHServerEmu.Core.Logging
|
||||
{
|
||||
|
|
@ -8,7 +9,8 @@ namespace MHServerEmu.Core.Logging
|
|||
/// </summary>
|
||||
internal static class LogRouter
|
||||
{
|
||||
private static readonly ConcurrentQueue<LogMessage> MessageQueue;
|
||||
private static readonly BlockingCollection<LogMessage> LogMessages;
|
||||
private static readonly Thread LogThread;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes <see cref="LogRouter"/>.
|
||||
|
|
@ -17,51 +19,53 @@ namespace MHServerEmu.Core.Logging
|
|||
{
|
||||
// Initialize async logging if synchronous mode is not enabled
|
||||
var config = ConfigManager.Instance.GetConfig<LoggingConfig>();
|
||||
if (config.SynchronousMode == false)
|
||||
if (config.SynchronousMode)
|
||||
return;
|
||||
|
||||
LogMessages = new();
|
||||
|
||||
LogThread = new(RouteLogMessages)
|
||||
{
|
||||
MessageQueue = new();
|
||||
Task.Run(async () => await RouteMessagesAsync());
|
||||
}
|
||||
Name = "Logging",
|
||||
IsBackground = true,
|
||||
CurrentCulture = CultureInfo.InvariantCulture
|
||||
};
|
||||
|
||||
LogThread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="LogMessage"/> instance from the provided arguments and processes it.
|
||||
/// Add a <see cref="LogMessage"/> instance to be routed.
|
||||
/// </summary>
|
||||
internal static void AddMessage(LoggingLevel level, string logger, string message, LogChannels channels, LogCategory category)
|
||||
internal static void AddLogMessage(in LogMessage logMessage)
|
||||
{
|
||||
if (LogManager.Enabled == false) return;
|
||||
if (LogManager.Enabled == false)
|
||||
return;
|
||||
|
||||
LogMessage logMessage = new(level, logger, message, channels, category);
|
||||
|
||||
if (MessageQueue != null)
|
||||
MessageQueue.Enqueue(logMessage); // Add the message to the queue to be processed asynchronously
|
||||
if (LogMessages != null)
|
||||
LogMessages.Add(logMessage); // Add the message to the queue to be routed asynchronously
|
||||
else
|
||||
RouteMessage(logMessage); // Process the message right away if async output is disabled (note: this is slow and should be used only for testing)
|
||||
RouteLogMessage(logMessage); // Route the message right away if async output is disabled (this is slow and should be used only for testing)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routes the provided <see cref="LogMessage"/> instance to all relevant targets.
|
||||
/// </summary>
|
||||
private static void RouteMessage(in LogMessage message)
|
||||
private static void RouteLogMessage(in LogMessage logMessage)
|
||||
{
|
||||
foreach (LogTarget target in LogManager.IterateTargets(message))
|
||||
target.ProcessLogMessage(message);
|
||||
foreach (LogTarget target in LogManager.IterateTargets(logMessage))
|
||||
target.ProcessLogMessage(logMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes enqueued <see cref="LogMessage"/> instances asynchronously.
|
||||
/// Processes enqueued <see cref="LogMessage"/>. This should run on its own thread.
|
||||
/// </summary>
|
||||
private static async Task RouteMessagesAsync()
|
||||
private static void RouteLogMessages()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
while (MessageQueue.IsEmpty == false)
|
||||
{
|
||||
if (MessageQueue.TryDequeue(out LogMessage message))
|
||||
RouteMessage(message);
|
||||
}
|
||||
|
||||
await Task.Delay(1);
|
||||
LogMessage logMessage = LogMessages.Take();
|
||||
RouteLogMessage(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,7 +173,8 @@ namespace MHServerEmu.Core.Logging
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Log(LoggingLevel level, string message, LogChannels channels = LogChannels.General, LogCategory category = LogCategory.Common)
|
||||
{
|
||||
LogRouter.AddMessage(level, _name, message, channels, category);
|
||||
LogMessage logMessage = new(level, _name, message, channels, category);
|
||||
LogRouter.AddLogMessage(logMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
public override void ProcessLogMessage(in LogMessage message)
|
||||
{
|
||||
SetForegroundColor(message.Level);
|
||||
Console.WriteLine(message.ToString(IncludeTimestamps));
|
||||
message.WriteTo(Console.Out, IncludeTimestamps, true);
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ namespace MHServerEmu.Core.Logging.Targets
|
|||
{
|
||||
string filePath = Path.Combine(logDirectory, $"{fileName}_{category}.log");
|
||||
FileStream fs = new(filePath, fileMode, FileAccess.Write, FileShare.Read);
|
||||
_writers[(int)category] = new(fs) { AutoFlush = true };
|
||||
_writers[(int)category] = new(fs);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -39,7 +39,7 @@ namespace MHServerEmu.Core.Logging.Targets
|
|||
// Create a single writer for all categories
|
||||
string filePath = Path.Combine(logDirectory, $"{fileName}.log");
|
||||
FileStream fs = new(filePath, fileMode, FileAccess.Write, FileShare.Read);
|
||||
_writers = [new(fs) { AutoFlush = true }];
|
||||
_writers = [new(fs)];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,8 +51,9 @@ namespace MHServerEmu.Core.Logging.Targets
|
|||
if (_disposed)
|
||||
return;
|
||||
|
||||
int index = _splitOutput ? (int)message.Category : 0;
|
||||
_writers[index].WriteLine(message.ToString(IncludeTimestamps));
|
||||
StreamWriter writer = _writers[_splitOutput ? (int)message.Category : 0];
|
||||
message.WriteTo(writer, IncludeTimestamps, true);
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyVersion>0.7.0.0</AssemblyVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Collections;
|
||||
using MHServerEmu.Core.Logging;
|
||||
|
||||
namespace MHServerEmu.Core.Memory
|
||||
{
|
||||
|
|
@ -9,7 +10,7 @@ namespace MHServerEmu.Core.Memory
|
|||
{
|
||||
// NOTE: We use a separate class to have shared settings for various CollectionPool types.
|
||||
|
||||
// For game threads we want to have dedicated pools, in other cases we'll use shared pools with locks
|
||||
// For game threads we want to have dedicated pools, in other cases we use shared pools with locks
|
||||
[ThreadStatic]
|
||||
public static bool UseThreadLocalStorage;
|
||||
}
|
||||
|
|
@ -43,6 +44,16 @@ namespace MHServerEmu.Core.Memory
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <typeparamref name="TCollection"/> from the pool or allocates a new one if the pool is empty.
|
||||
/// Returns a <see cref="CollectionHandle"/> that can automatically return the <typeparamref name="TCollection"/> instance to the pool when it goes out of scope.
|
||||
/// </summary>
|
||||
public CollectionHandle Get(out TCollection collection)
|
||||
{
|
||||
collection = Get();
|
||||
return new(this, collection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the provided <typeparamref name="TCollection"/> and returns it to the pool.
|
||||
/// </summary>
|
||||
|
|
@ -61,7 +72,27 @@ namespace MHServerEmu.Core.Memory
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a storage unit of a pool or a particular type.
|
||||
/// A handle that implements <see cref="IDisposable"/> that can automatically return a <typeparamref name="TCollection"/> instance to the pool when it goes out of scope.
|
||||
/// </summary>
|
||||
public readonly struct CollectionHandle : IDisposable
|
||||
{
|
||||
private readonly CollectionPool<TCollection, TValue> _pool;
|
||||
private readonly TCollection _collection;
|
||||
|
||||
public CollectionHandle(CollectionPool<TCollection, TValue> pool, TCollection collection)
|
||||
{
|
||||
_pool = pool;
|
||||
_collection = collection;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_pool.Return(_collection);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a storage unit of a pool of a particular type.
|
||||
/// </summary>
|
||||
private class Node
|
||||
{
|
||||
|
|
@ -111,7 +142,7 @@ namespace MHServerEmu.Core.Memory
|
|||
private ListPool() { }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <typeparamref name="TCollection"/> from the pool or allocates a new one if the pool is empty and ensures it has the specified capacity.
|
||||
/// Retrieves a <see cref="List{T}"/> from the pool or allocates a new one if the pool is empty and ensures it has the specified capacity.
|
||||
/// </summary>
|
||||
public List<T> Get(int capacity)
|
||||
{
|
||||
|
|
@ -121,7 +152,19 @@ namespace MHServerEmu.Core.Memory
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="List{T}"/> from the pool or allocates a new one if the pool is empty and copies all elements from collection.
|
||||
/// Retrieves a <see cref="List{T}"/> from the pool or allocates a new one if the pool is empty and ensures it has the specified capacity.
|
||||
/// Returns a <see cref="CollectionPool{TCollection, TValue}.CollectionHandle"/> that can automatically return the <see cref="List{T}"/>
|
||||
/// instance to the pool when it goes out of scope.
|
||||
/// </summary>
|
||||
public CollectionHandle Get(int capacity, out List<T> list)
|
||||
{
|
||||
CollectionHandle handle = Get(out list);
|
||||
list.EnsureCapacity(capacity);
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="List{T}"/> from the pool or allocates a new one if the pool is empty and copies all elements from the provided <see cref="IEnumerable{T}"/> collection.
|
||||
/// </summary>
|
||||
public List<T> Get(IEnumerable<T> collection)
|
||||
{
|
||||
|
|
@ -129,6 +172,17 @@ namespace MHServerEmu.Core.Memory
|
|||
list.AddRange(collection);
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="List{T}"/> from the pool or allocates a new one if the pool is empty and copies all elements from the provided <see cref="IEnumerable{T}"/> collection.
|
||||
/// Returns a <see cref="CollectionPool{TCollection, TValue}.CollectionHandle"/> that can automatically return the <see cref="List{T}"/> instance to the pool when it goes out of scope.
|
||||
/// </summary>
|
||||
public CollectionHandle Get(IEnumerable<T> collection, out List<T> list)
|
||||
{
|
||||
CollectionHandle handle = Get(out list);
|
||||
list.AddRange(collection);
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -150,4 +204,14 @@ namespace MHServerEmu.Core.Memory
|
|||
|
||||
private HashSetPool() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a pool of reusable <see cref="PoolableStack{T}"/> instances, similar to ArrayPool.
|
||||
/// </summary>
|
||||
public sealed class StackPool<T> : CollectionPool<PoolableStack<T>, T>
|
||||
{
|
||||
public static StackPool<T> Instance { get; } = new();
|
||||
|
||||
private StackPool() { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
src/MHServerEmu.Core/Memory/ConcurrentPool.cs
Normal file
37
src/MHServerEmu.Core/Memory/ConcurrentPool.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace MHServerEmu.Core.Memory
|
||||
{
|
||||
/// <summary>
|
||||
/// A thread-safe pool of arbitrary objects intended to be used for cases where retrieval and returns always happen on separate threads.
|
||||
/// </summary>
|
||||
public class ConcurrentPool<T>
|
||||
{
|
||||
private readonly ConcurrentStack<T> _items = new();
|
||||
private readonly int _maxCount;
|
||||
private readonly Func<T> _constructor;
|
||||
|
||||
public ConcurrentPool(int maxCount, Func<T> constructor)
|
||||
{
|
||||
Debug.Assert(maxCount > 0);
|
||||
|
||||
_maxCount = maxCount;
|
||||
_constructor = constructor;
|
||||
}
|
||||
|
||||
public T Get()
|
||||
{
|
||||
if (_items.TryPop(out T item) == false)
|
||||
return _constructor();
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public void Return(T item)
|
||||
{
|
||||
if (_items.Count < _maxCount)
|
||||
_items.Push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/MHServerEmu.Core/Memory/PoolSpan.cs
Normal file
56
src/MHServerEmu.Core/Memory/PoolSpan.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System.Buffers;
|
||||
|
||||
namespace MHServerEmu.Core.Memory
|
||||
{
|
||||
/// <summary>
|
||||
/// A wrapper around <see cref="Span{T}"/> for automatically slicing and disposing memory rented from <see cref="ArrayPool{T}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is similar to SpanOwner from the .NET Community Toolkit.
|
||||
/// </remarks>
|
||||
public readonly ref struct PoolSpan<T>
|
||||
{
|
||||
private readonly ArrayPool<T> _pool;
|
||||
private readonly T[] _buffer;
|
||||
private readonly bool _clearOnDispose;
|
||||
|
||||
public Span<T> Span { get; }
|
||||
|
||||
public int Length { get => Span.Length; }
|
||||
public T this[int index] { get => Span[index]; set => Span[index] = value; }
|
||||
|
||||
private PoolSpan(ArrayPool<T> pool, T[] buffer, int length, bool clearOnDispose)
|
||||
{
|
||||
_pool = pool;
|
||||
_buffer = buffer;
|
||||
Span = buffer.AsSpan(0, length);
|
||||
_clearOnDispose = clearOnDispose;
|
||||
}
|
||||
|
||||
public static PoolSpan<T> Allocate(int length, bool clearOnDispose, ArrayPool<T> pool)
|
||||
{
|
||||
T[] buffer = pool.Rent(length);
|
||||
return new(pool, buffer, length, clearOnDispose);
|
||||
}
|
||||
|
||||
public static PoolSpan<T> Allocate(int length, bool clearOnDispose = true)
|
||||
{
|
||||
return Allocate(length, clearOnDispose, ArrayPool<T>.Shared);
|
||||
}
|
||||
|
||||
public Span<T>.Enumerator GetEnumerator()
|
||||
{
|
||||
return Span.GetEnumerator();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Span.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_pool.Return(_buffer, _clearOnDispose);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Text;
|
||||
using MHServerEmu.Core.Helpers;
|
||||
using MHServerEmu.Core.Logging;
|
||||
|
||||
namespace MHServerEmu.Core.Metrics.Categories
|
||||
|
|
@ -79,7 +78,7 @@ namespace MHServerEmu.Core.Metrics.Categories
|
|||
return _trackers[(int)metric].AsReportEntry();
|
||||
}
|
||||
|
||||
public readonly struct Report : IHtmlDataStructure
|
||||
public readonly struct Report
|
||||
{
|
||||
public MetricTracker.ReportEntry UpdateTime { get; }
|
||||
public MetricTracker.ReportEntry FrameTime { get; }
|
||||
|
|
@ -106,21 +105,6 @@ namespace MHServerEmu.Core.Metrics.Categories
|
|||
sb.AppendLine($"{nameof(PlayerCount)}: {PlayerCount}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void BuildHtml(StringBuilder sb)
|
||||
{
|
||||
HtmlBuilder.BeginTable(sb);
|
||||
|
||||
HtmlBuilder.AppendTableRow(sb, "Metric", "Avg", "Mdn", "Last", "Min", "Max");
|
||||
|
||||
HtmlBuilder.AppendDataStructure(sb, UpdateTime);
|
||||
HtmlBuilder.AppendDataStructure(sb, FrameTime);
|
||||
HtmlBuilder.AppendDataStructure(sb, ScheduledEventsPerUpdate);
|
||||
HtmlBuilder.AppendDataStructure(sb, EntityCount);
|
||||
HtmlBuilder.AppendDataStructure(sb, PlayerCount);
|
||||
|
||||
HtmlBuilder.EndTable(sb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Runtime;
|
||||
using System.Text;
|
||||
using MHServerEmu.Core.Helpers;
|
||||
using MHServerEmu.Core.Logging;
|
||||
|
||||
namespace MHServerEmu.Core.Metrics.Categories
|
||||
|
|
@ -63,7 +62,7 @@ namespace MHServerEmu.Core.Metrics.Categories
|
|||
}
|
||||
}
|
||||
|
||||
public readonly struct Report : IHtmlDataStructure
|
||||
public readonly struct Report
|
||||
{
|
||||
public long GCIndex { get; }
|
||||
public long GCCountGen0 { get; }
|
||||
|
|
@ -98,19 +97,6 @@ namespace MHServerEmu.Core.Metrics.Categories
|
|||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void BuildHtml(StringBuilder sb)
|
||||
{
|
||||
HtmlBuilder.BeginUnorderedList(sb);
|
||||
|
||||
HtmlBuilder.AppendListItem(sb, $"{nameof(GCIndex)}: {GCIndex}");
|
||||
HtmlBuilder.AppendListItem(sb, $"GCCounts: Gen0={GCCountGen0}, Gen1={GCCountGen1}, Gen2={GCCountGen2}");
|
||||
HtmlBuilder.AppendListItem(sb, $"{nameof(HeapSizeBytes)}: {HeapSizeBytes:N0} / {TotalCommittedBytes:N0}");
|
||||
HtmlBuilder.AppendListItem(sb, $"{nameof(PauseTimePercentage)}: {PauseTimePercentage}%");
|
||||
HtmlBuilder.AppendListItem(sb, $"{nameof(PauseDuration)}: {PauseDuration}");
|
||||
|
||||
HtmlBuilder.EndUnorderedList(sb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using System.Text;
|
||||
using MHServerEmu.Core.Collections;
|
||||
using MHServerEmu.Core.Collections;
|
||||
using MHServerEmu.Core.Extensions;
|
||||
using MHServerEmu.Core.Helpers;
|
||||
|
||||
namespace MHServerEmu.Core.Metrics
|
||||
{
|
||||
|
|
@ -60,9 +58,8 @@ namespace MHServerEmu.Core.Metrics
|
|||
/// <summary>
|
||||
/// A snapshot of the state of a <see cref="MetricTracker"/>.
|
||||
/// </summary>
|
||||
public readonly struct ReportEntry : IHtmlDataStructure
|
||||
public readonly struct ReportEntry
|
||||
{
|
||||
public string Name { get; }
|
||||
public float Average { get; }
|
||||
public float Median { get; }
|
||||
public float Last { get; }
|
||||
|
|
@ -71,7 +68,6 @@ namespace MHServerEmu.Core.Metrics
|
|||
|
||||
public ReportEntry(MetricTracker tracker)
|
||||
{
|
||||
Name = tracker._name;
|
||||
Average = tracker._buffer.ToAverage();
|
||||
Median = tracker._buffer.ToMedian();
|
||||
Last = tracker._last;
|
||||
|
|
@ -83,17 +79,6 @@ namespace MHServerEmu.Core.Metrics
|
|||
{
|
||||
return $"avg={Average}, mdn={Median}, last={Last}, min={Min}, max={Max}";
|
||||
}
|
||||
|
||||
public void BuildHtml(StringBuilder sb)
|
||||
{
|
||||
HtmlBuilder.AppendTableRow(sb,
|
||||
Name,
|
||||
Average.ToString("0.00"),
|
||||
Median.ToString("0.00"),
|
||||
Last.ToString("0.00"),
|
||||
Min != float.MaxValue ? Min.ToString("0.00") : "0.00",
|
||||
Max != float.MinValue ? Max.ToString("0.00") : "0.00");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
{
|
||||
PlainText,
|
||||
Json,
|
||||
Html,
|
||||
}
|
||||
|
||||
public enum GamePerformanceMetricEnum
|
||||
|
|
|
|||
|
|
@ -76,12 +76,17 @@ namespace MHServerEmu.Core.Metrics
|
|||
_gameInstancesToRemove.Enqueue(gameId);
|
||||
}
|
||||
|
||||
public void GetPerformanceReportData(PerformanceReport report)
|
||||
{
|
||||
lock (_lock)
|
||||
report.Initialize(_memoryMetrics, _gamePerformanceMetricsDict);
|
||||
}
|
||||
|
||||
public string GeneratePerformanceReport(MetricsReportFormat format)
|
||||
{
|
||||
using PerformanceReport report = ObjectPoolManager.Instance.Get<PerformanceReport>();
|
||||
|
||||
lock (_lock)
|
||||
report.Initialize(_memoryMetrics, _gamePerformanceMetricsDict.Values);
|
||||
GetPerformanceReportData(report);
|
||||
|
||||
return report.ToString(format);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using MHServerEmu.Core.Helpers;
|
||||
using System.Text.Json.Serialization;
|
||||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Memory;
|
||||
using MHServerEmu.Core.Metrics.Categories;
|
||||
|
|
@ -14,21 +14,23 @@ namespace MHServerEmu.Core.Metrics
|
|||
|
||||
private static uint _currentReportId = 0;
|
||||
|
||||
[JsonNumberHandling(JsonNumberHandling.WriteAsString)]
|
||||
public ulong Id { get; private set; }
|
||||
public MemoryMetrics.Report Memory { get; private set; }
|
||||
public Dictionary<ulong, GamePerformanceMetrics.Report> Games { get; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsInPool { get; set; }
|
||||
|
||||
public PerformanceReport() { }
|
||||
|
||||
public void Initialize(MemoryMetrics memoryMetrics, IEnumerable<GamePerformanceMetrics> gameMetrics)
|
||||
public void Initialize(MemoryMetrics memoryMetrics, Dictionary<ulong, GamePerformanceMetrics> gameMetrics)
|
||||
{
|
||||
Id = (ulong)Clock.UnixTime.TotalSeconds << 32 | ++_currentReportId;
|
||||
|
||||
Memory = memoryMetrics.GetReport();
|
||||
|
||||
foreach (GamePerformanceMetrics metrics in gameMetrics)
|
||||
foreach (GamePerformanceMetrics metrics in gameMetrics.Values)
|
||||
{
|
||||
Games.Add(metrics.GameId, metrics.GetReport());
|
||||
}
|
||||
|
|
@ -49,9 +51,6 @@ namespace MHServerEmu.Core.Metrics
|
|||
case MetricsReportFormat.Json:
|
||||
return JsonSerializer.Serialize(this);
|
||||
|
||||
case MetricsReportFormat.Html:
|
||||
return AsHtml();
|
||||
|
||||
default:
|
||||
return Logger.WarnReturn(string.Empty, $"ToString(): Unsupported format {format}");
|
||||
}
|
||||
|
|
@ -85,24 +84,5 @@ namespace MHServerEmu.Core.Metrics
|
|||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string AsHtml()
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
|
||||
HtmlBuilder.AppendParagraph(sb, $"Report 0x{Id:X}");
|
||||
|
||||
HtmlBuilder.AppendHeader2(sb, "Memory");
|
||||
HtmlBuilder.AppendDataStructure(sb, Memory);
|
||||
|
||||
HtmlBuilder.AppendHeader2(sb, "Games");
|
||||
foreach (var kvp in Games.OrderBy(kvp => kvp.Key))
|
||||
{
|
||||
HtmlBuilder.AppendHeader3(sb, $"0x{kvp.Key:X}");
|
||||
HtmlBuilder.AppendDataStructure(sb, kvp.Value);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,22 @@ namespace MHServerEmu.Core.Network
|
|||
RemoveResponse,
|
||||
}
|
||||
|
||||
public enum ChatRoomOperationType
|
||||
{
|
||||
Add,
|
||||
Remove,
|
||||
}
|
||||
|
||||
public enum AccountOperation
|
||||
{
|
||||
Create,
|
||||
SetPlayerName,
|
||||
SetPassword,
|
||||
SetUserLevel,
|
||||
SetFlag,
|
||||
ClearFlag,
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static class ServiceMessage
|
||||
|
|
@ -65,7 +81,7 @@ namespace MHServerEmu.Core.Network
|
|||
public readonly MailboxMessage Message = message;
|
||||
}
|
||||
|
||||
#region Game Instances
|
||||
#region Player Manager
|
||||
|
||||
public readonly struct GameInstanceOp(GameInstanceOpType type, ulong gameId)
|
||||
: IGameServiceMessage
|
||||
|
|
@ -104,6 +120,16 @@ namespace MHServerEmu.Core.Network
|
|||
public readonly bool Success = success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Game -> PlayerManager] Requests <see cref="RegionPlayerAccessVar"/> update for a region.
|
||||
/// </summary>
|
||||
public readonly struct SetRegionPlayerAccess(ulong regionId, RegionPlayerAccessVar playerAccess)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RegionId = regionId;
|
||||
public readonly RegionPlayerAccessVar PlayerAccess = playerAccess;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Game -> PlayerManager] Requests the player manager to shut down the specified region.
|
||||
/// </summary>
|
||||
|
|
@ -230,6 +256,15 @@ namespace MHServerEmu.Core.Network
|
|||
public readonly ulong DifficultyTierProtoId = difficultyTierProtoId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Game -> PlayerManager] Notifies the Player Manager that a save happened and player data needs to be written to the database.
|
||||
/// </summary>
|
||||
public readonly struct PlayerDataUpdated(ulong playerDbId)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Game -> PlayerManager] Requests player dbid and properly cased name from the player manager.
|
||||
/// </summary>
|
||||
|
|
@ -328,6 +363,9 @@ namespace MHServerEmu.Core.Network
|
|||
public readonly PartyOperationPayload Request = request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Game -> PlayerManager] Notifies the Player Manager of a player's current party boosts.
|
||||
/// </summary>
|
||||
public readonly struct PartyBoostUpdate(ulong playerDbId, List<ulong> boosts)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
|
|
@ -375,33 +413,159 @@ namespace MHServerEmu.Core.Network
|
|||
public readonly PartyMemberInfo MemberInfo = memberInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [PlayerManager -> Game] Notifies a player of a party kick grace period before they are removed from the current region.
|
||||
/// </summary>
|
||||
public readonly struct PartyKickGracePeriod(ulong gameId, ulong playerDbId, ulong expireTimeMicroseconds, GroupLeaveReason leaveReason)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong GameId = gameId;
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
public readonly ulong ExpireTimeMicroseconds = expireTimeMicroseconds;
|
||||
public readonly GroupLeaveReason LeaveReason = leaveReason;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Game -> PlayerManager] Routes guild messages to the player manager.
|
||||
/// </summary>
|
||||
public readonly struct GuildMessageToPlayerManager(GuildMessageSetToPlayerManager messages)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly GuildMessageSetToPlayerManager Messages = messages;
|
||||
}
|
||||
|
||||
// NOTE: In the protocol for 1.53 there is a single GuildMessageToServer protobuf that is used for both client and server guild messages.
|
||||
// Server messages don't actually need a list of players, and it makes more sense to split them.
|
||||
|
||||
/// <summary>
|
||||
/// [PlayerManager -> Game] Routes guild messages from the player manager to a game instance.
|
||||
/// </summary>
|
||||
public readonly struct GuildMessageToServer(ulong gameId, GuildMessageSetToServer serverMessages)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong GameId = gameId;
|
||||
public readonly GuildMessageSetToServer Messages = serverMessages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [PlayerManager -> Game] Routes guilds messages from the player manager to a client in a game instance.
|
||||
/// </summary>
|
||||
public readonly struct GuildMessageToClient(ulong gameId, ulong playerDbId, GuildMessageSetToClient messages)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong GameId = gameId;
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
public readonly GuildMessageSetToClient Messages = messages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Game -> PlayerManager] Relays a match region request command from a client.
|
||||
/// </summary>
|
||||
public readonly struct MatchRegionRequestQueueCommand(ulong playerDbId, ulong regionProtoId, ulong difficultyTierProtoId, ulong metaStateProtoId, RegionRequestQueueCommandVar command, ulong regionRequestGroupId, ulong targetPlayerDbId, int teamSizeOverride)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
public readonly ulong RegionProtoId = regionProtoId;
|
||||
public readonly ulong DifficultyTierProtoId = difficultyTierProtoId;
|
||||
public readonly ulong MetaStateProtoId = metaStateProtoId;
|
||||
public readonly RegionRequestQueueCommandVar Command = command;
|
||||
public readonly ulong RegionRequestGroupId = regionRequestGroupId;
|
||||
public readonly ulong TargetPlayerDbId = targetPlayerDbId;
|
||||
public readonly int TeamSizeOverride = teamSizeOverride;
|
||||
}
|
||||
|
||||
// MatchQueueUpdate is based on PlayerMgrToGameServer.proto from 1.53
|
||||
public readonly struct MatchQueueUpdateData(ulong updatePlayerGuid, RegionRequestQueueUpdateVar status, string updatePlayerName = null)
|
||||
{
|
||||
public readonly ulong UpdatePlayerGuid = updatePlayerGuid;
|
||||
public readonly RegionRequestQueueUpdateVar Status = status;
|
||||
public readonly string UpdatePlayerName = updatePlayerName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [PlayerManager -> Game] Updates the state of a MatchQueueStatus instance game-side.
|
||||
/// </summary>
|
||||
public readonly struct MatchQueueUpdate(ulong gameId, ulong playerDbId, ulong regionProtoId, ulong difficultyTierProtoId, int playersInQueue, ulong regionRequestGroupId, List<MatchQueueUpdateData> data)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong GameId = gameId;
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
public readonly ulong RegionProtoId = regionProtoId;
|
||||
public readonly ulong DifficultyTierProtoId = difficultyTierProtoId;
|
||||
public readonly int PlayersInQueue = playersInQueue;
|
||||
public readonly ulong RegionRequestGroupId = regionRequestGroupId;
|
||||
public readonly List<MatchQueueUpdateData> Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [PlayerManager -> Game] Clears the state of a MatchQueueStatus instance game-side.
|
||||
/// </summary>
|
||||
public readonly struct MatchQueueFlush(ulong gameId, ulong playerDbId)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong GameId = gameId;
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Grouping Manager
|
||||
|
||||
public readonly struct GroupingManagerChat(IFrontendClient client, NetMessageChat chat, int prestigeLevel, List<ulong> playerFilter)
|
||||
/// <summary>
|
||||
/// [Game -> GroupingManager] Routes a regular chat message from a game instance.
|
||||
/// </summary>
|
||||
public readonly struct GroupingManagerChat(ulong playerDbId, NetMessageChat chat, int prestigeLevel, List<ulong> playerFilter)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly IFrontendClient Client = client;
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
public readonly NetMessageChat Chat = chat;
|
||||
public readonly int PrestigeLevel = prestigeLevel;
|
||||
public readonly List<ulong> PlayerFilter = playerFilter;
|
||||
}
|
||||
|
||||
public readonly struct GroupingManagerTell(IFrontendClient client, NetMessageTell tell, int prestigeLevel)
|
||||
/// <summary>
|
||||
/// [Game -> GroupingManager] Routes a tell chat message from a game instance.
|
||||
/// </summary>
|
||||
public readonly struct GroupingManagerTell(ulong playerDbId, NetMessageTell tell, int prestigeLevel)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly IFrontendClient Client = client;
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
public readonly NetMessageTell Tell = tell;
|
||||
public readonly int PrestigeLevel = prestigeLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Any -> GroupingManager] Sends a custom metagame chat message to the specified player.
|
||||
/// </summary>
|
||||
public readonly struct GroupingManagerMetagameMessage(ulong playerDbId, string text, bool showSender)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
public readonly string Text = text;
|
||||
public readonly bool ShowSender = showSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Command -> GroupingManager] Broadcasts a server notification to all connected clients.
|
||||
/// </summary>
|
||||
public readonly struct GroupingManagerServerNotification(string notificationText)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly string NotificationText = notificationText;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [PlayerManager -> GroupingManager] Adds/removes a player to/from the specified chat room.
|
||||
/// </summary>
|
||||
public readonly struct GroupingManagerChatRoomOperation(ChatRoomTypes roomType, ulong roomId, ulong playerDbId, ChatRoomOperationType operation)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ChatRoomTypes RoomType = roomType;
|
||||
public readonly ulong RoomId = roomId;
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
public readonly ChatRoomOperationType Operation = operation;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Leaderboards
|
||||
|
|
@ -530,5 +694,149 @@ namespace MHServerEmu.Core.Network
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Auth
|
||||
|
||||
// WebFrontend -> PlayerManager
|
||||
public readonly struct AuthRequest(ulong requestId, LoginDataPB loginDataPB)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly LoginDataPB LoginDataPB = loginDataPB;
|
||||
}
|
||||
|
||||
// PlayerManager -> WebFrontend
|
||||
public readonly struct AuthResponse(ulong requestId, int statusCode, AuthTicket authTicket)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly int StatusCode = statusCode;
|
||||
public readonly AuthTicket AuthTicket = authTicket;
|
||||
}
|
||||
|
||||
// Frontend -> PlayerManager
|
||||
public readonly struct SessionVerificationRequest(IFrontendClient client, ClientCredentials clientCredentials)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly IFrontendClient Client = client;
|
||||
public readonly ClientCredentials ClientCredentials = clientCredentials;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MTXStore
|
||||
|
||||
// WebFrontend -> PlayerManager
|
||||
public readonly struct MTXStoreESBalanceRequest(ulong requestId, string email, string token)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly string Email = email;
|
||||
public readonly string Token = token;
|
||||
}
|
||||
|
||||
// PlayerManager -> WebFrontend
|
||||
public readonly struct MTXStoreESBalanceResponse(ulong requestId, int statusCode, int currentBalance = 0, float conversionRatio = 0, int conversionStep = 0)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly int StatusCode = statusCode;
|
||||
public readonly int CurrentBalance = currentBalance;
|
||||
public readonly float ConversionRatio = conversionRatio;
|
||||
public readonly int ConversionStep = conversionStep;
|
||||
}
|
||||
|
||||
// PlayerManager -> Game
|
||||
public readonly struct MTXStoreESBalanceGameRequest(ulong requestId, ulong gameId, ulong playerDbId)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly ulong GameId = gameId;
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
}
|
||||
|
||||
// Game -> PlayerManager
|
||||
public readonly struct MTXStoreESBalanceGameResponse(ulong requestId, int currentBalance, float conversionRatio, int conversionStep)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly int CurrentBalance = currentBalance;
|
||||
public readonly float ConversionRatio = conversionRatio;
|
||||
public readonly int ConversionStep = conversionStep;
|
||||
}
|
||||
|
||||
// WebFrontend -> PlayerManager
|
||||
public readonly struct MTXStoreESConvertRequest(ulong requestId, string email, string token, int amount)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly string Email = email;
|
||||
public readonly string Token = token;
|
||||
public readonly int Amount = amount;
|
||||
}
|
||||
|
||||
// PlayerManager -> WebFrontend
|
||||
public readonly struct MTXStoreESConvertResponse(ulong requestId, int statusCode)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly int StatusCode = statusCode;
|
||||
}
|
||||
|
||||
// PlayerManager -> Game
|
||||
public readonly struct MTXStoreESConvertGameRequest(ulong requestId, ulong gameId, ulong playerDbId, int amount)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly ulong GameId = gameId;
|
||||
public readonly ulong PlayerDbId = playerDbId;
|
||||
public readonly int Amount = amount;
|
||||
}
|
||||
|
||||
// Game -> PlayerManager
|
||||
public readonly struct MTXStoreESConvertGameResponse(ulong requestId, bool result)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly bool Result = result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Account
|
||||
|
||||
/// <summary>
|
||||
/// [WebFrontend -> PlayerManager] Routes an account operation request to the Player Manager service.
|
||||
/// </summary>
|
||||
public readonly struct AccountOperationRequest(ulong requestId, AccountOperation operation, string email,
|
||||
string playerName, string password, byte userLevel, int flags)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly AccountOperation Operation = operation;
|
||||
public readonly string Email = email;
|
||||
public readonly string PlayerName = playerName;
|
||||
public readonly string Password = password;
|
||||
public readonly byte UserLevel = userLevel;
|
||||
public readonly int Flags = flags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [PlayerManager -> WebFrontend] Routes a response to an account operation request back to the Web Frontend.
|
||||
/// </summary>
|
||||
public readonly struct AccountOperationResponse(ulong requestId, int resultCode)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly ulong RequestId = requestId;
|
||||
public readonly int ResultCode = resultCode;
|
||||
}
|
||||
|
||||
public readonly struct SetWhitelistEnabled(bool enable)
|
||||
: IGameServiceMessage
|
||||
{
|
||||
public readonly bool Enable = enable;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,5 +7,6 @@
|
|||
{
|
||||
public ulong Id { get; }
|
||||
public object Account { get; } // Not having this be strongly typed is not ideal, but it allows us to avoid coupling Core and DatabaseAccess.
|
||||
public string Locale { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@
|
|||
public void ReceiveServiceMessage<T>(in T message) where T: struct, IGameServiceMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="string"/> representing the status of this <see cref="IGameService"/>.
|
||||
/// Adds the status of this <see cref="IGameService"/> to the provided dictionary.
|
||||
/// </summary>
|
||||
public string GetStatus();
|
||||
public void GetStatus(Dictionary<string, long> statusDict);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Google.ProtocolBuffers;
|
||||
using MHServerEmu.Core.Serialization;
|
||||
|
||||
namespace MHServerEmu.Core.Network
|
||||
{
|
||||
|
|
@ -43,5 +44,15 @@ namespace MHServerEmu.Core.Network
|
|||
stream.WriteRawVarint32((uint)Message.SerializedSize);
|
||||
Message.WriteTo(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes this <see cref="MessagePackageOut"/> to the provided <see cref="ICodedOutputStreamEx"/>.
|
||||
/// </summary>
|
||||
public void WriteTo(ICodedOutputStreamEx stream)
|
||||
{
|
||||
stream.WriteRawVarint32(Id);
|
||||
stream.WriteRawVarint32((uint)Message.SerializedSize);
|
||||
Message.WriteTo(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
using Google.ProtocolBuffers;
|
||||
using MHServerEmu.Core.Helpers;
|
||||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Memory;
|
||||
using MHServerEmu.Core.Network.Tcp;
|
||||
using MHServerEmu.Core.Serialization;
|
||||
|
||||
namespace MHServerEmu.Core.Network
|
||||
{
|
||||
|
|
@ -13,7 +13,9 @@ namespace MHServerEmu.Core.Network
|
|||
public readonly struct MuxPacket : IPacket
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
private static readonly ArrayPool<byte> BufferPool = ArrayPool<byte>.Create();
|
||||
|
||||
// Packets apparently go as high as 2800+ messages based on logs, so we presize pooled lists to 4096 to fit that and extra.
|
||||
private static readonly ConcurrentPool<List<MessagePackageOut>> MessageListPool = new(4096, static () => new(4096));
|
||||
|
||||
private readonly List<MessagePackageOut> _outboundMessageList = null;
|
||||
|
||||
|
|
@ -39,7 +41,16 @@ namespace MHServerEmu.Core.Network
|
|||
Command = command;
|
||||
|
||||
if (IsDataPacket)
|
||||
_outboundMessageList = new();
|
||||
_outboundMessageList = MessageListPool.Get();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsDataPacket)
|
||||
{
|
||||
_outboundMessageList.Clear();
|
||||
MessageListPool.Return(_outboundMessageList);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -63,8 +74,6 @@ namespace MHServerEmu.Core.Network
|
|||
if (IsDataPacket == false)
|
||||
return Logger.WarnReturn(false, "AddMessages(): Attempted to add messages to a non-data packet");
|
||||
|
||||
_outboundMessageList.EnsureCapacity(_outboundMessageList.Count + messageList.Count);
|
||||
|
||||
foreach (IMessage message in messageList)
|
||||
{
|
||||
MessagePackageOut messagePackage = new(message);
|
||||
|
|
@ -126,15 +135,10 @@ namespace MHServerEmu.Core.Network
|
|||
if (_outboundMessageList.Count == 0)
|
||||
return Logger.WarnReturn(false, "SerializeData(): Data packet contains no messages");
|
||||
|
||||
// Use pooled buffers for coded output streams with reflection hackery, see ProtobufHelper for more info.
|
||||
byte[] buffer = BufferPool.Rent(4096);
|
||||
using RecyclableCodedOutputStream cos = RecyclableCodedOutputStream.CreateInstance(stream);
|
||||
|
||||
CodedOutputStream cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(stream, buffer);
|
||||
foreach (MessagePackageOut messagePackage in _outboundMessageList)
|
||||
messagePackage.WriteTo(cos);
|
||||
cos.Flush();
|
||||
|
||||
BufferPool.Return(buffer);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Globalization;
|
||||
using System.Text;
|
||||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Metrics;
|
||||
using MHServerEmu.Core.Memory;
|
||||
using MHServerEmu.Core.System.Time;
|
||||
|
||||
namespace MHServerEmu.Core.Network
|
||||
|
|
@ -13,9 +13,8 @@ namespace MHServerEmu.Core.Network
|
|||
Leaderboard,
|
||||
PlayerManager,
|
||||
GroupingManager,
|
||||
Billing,
|
||||
Frontend,
|
||||
Auth,
|
||||
WebFrontend,
|
||||
NumServiceTypes
|
||||
}
|
||||
|
||||
|
|
@ -234,33 +233,30 @@ namespace MHServerEmu.Core.Network
|
|||
_state = ServerManagerState.Shutdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds structured server status data to the provided dictionary.
|
||||
/// </summary>
|
||||
public void GetServerStatus(Dictionary<string, long> statusDict)
|
||||
{
|
||||
statusDict["StartupTime"] = (long)StartupTime.TotalSeconds;
|
||||
statusDict["CurrentTime"] = (long)Clock.UnixTime.TotalSeconds;
|
||||
|
||||
for (int i = 0; i < _services.Length; i++)
|
||||
_services[i]?.GetStatus(statusDict);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="string"/> representing the current status of all running <see cref="IGameService"/> instances.
|
||||
/// </summary>
|
||||
public string GetServerStatus(bool includeMetrics)
|
||||
public string GetServerStatusString()
|
||||
{
|
||||
using var statusDictHandle = DictionaryPool<string, long>.Instance.Get(out Dictionary<string, long> statusDict);
|
||||
GetServerStatus(statusDict);
|
||||
|
||||
StringBuilder sb = new();
|
||||
|
||||
TimeSpan uptime = Clock.UnixTime - StartupTime;
|
||||
sb.AppendLine($"Uptime: {uptime:dd\\:hh\\:mm\\:ss}");
|
||||
|
||||
sb.AppendLine("Service Status:");
|
||||
for (int i = 0; i < _services.Length; i++)
|
||||
{
|
||||
if (_services[i] == null) continue;
|
||||
sb.Append($"[{(GameServiceType)i}] ");
|
||||
|
||||
if (_serviceThreads[i] != null)
|
||||
sb.AppendLine($"{_services[i].GetStatus()}");
|
||||
else
|
||||
sb.AppendLine("Not running");
|
||||
}
|
||||
|
||||
if (includeMetrics)
|
||||
{
|
||||
sb.AppendLine("Performance Metrics:");
|
||||
sb.AppendLine(MetricsManager.Instance.GeneratePerformanceReport(MetricsReportFormat.PlainText));
|
||||
}
|
||||
foreach (var kvp in statusDict)
|
||||
sb.AppendLine($"{kvp.Key}: {kvp.Value}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/// <summary>
|
||||
/// Exposes a packet's serialization routine.
|
||||
/// </summary>
|
||||
public interface IPacket
|
||||
public interface IPacket : IDisposable
|
||||
{
|
||||
public int SerializedSize { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using MHServerEmu.Core.Config;
|
||||
using MHServerEmu.Core.Extensions;
|
||||
using MHServerEmu.Core.Helpers;
|
||||
using MHServerEmu.Core.Logging;
|
||||
|
||||
namespace MHServerEmu.Core.Network.Tcp
|
||||
|
|
@ -37,10 +37,13 @@ namespace MHServerEmu.Core.Network.Tcp
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
if (HideSensitiveInformation)
|
||||
return RemoteEndPoint?.ToStringMasked();
|
||||
if (RemoteEndPoint == null)
|
||||
return "NULL";
|
||||
|
||||
return RemoteEndPoint?.ToString();
|
||||
if (HideSensitiveInformation)
|
||||
return $"0x{HashHelper.Djb2(RemoteEndPoint.Address.ToString()):X8}";
|
||||
|
||||
return RemoteEndPoint.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -345,13 +345,22 @@ namespace MHServerEmu.Core.Network.Tcp
|
|||
/// </summary>
|
||||
private async Task<int> SendAsync<T>(TcpClientConnection connection, T packet, SocketFlags flags = SocketFlags.None) where T: IPacket
|
||||
{
|
||||
int sent = 0;
|
||||
|
||||
int size = packet.SerializedSize;
|
||||
byte[] buffer = _bufferPool.Rent(size);
|
||||
|
||||
packet.Serialize(buffer);
|
||||
int sent = await SendAsync(connection, buffer, size, flags);
|
||||
try
|
||||
{
|
||||
packet.Serialize(buffer);
|
||||
sent = await SendAsync(connection, buffer, size, flags);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_bufferPool.Return(buffer);
|
||||
packet.Dispose();
|
||||
}
|
||||
|
||||
_bufferPool.Return(buffer);
|
||||
return sent;
|
||||
}
|
||||
|
||||
|
|
|
|||
21
src/MHServerEmu.Core/Network/Web/WebApiAccessType.cs
Normal file
21
src/MHServerEmu.Core/Network/Web/WebApiAccessType.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
namespace MHServerEmu.Core.Network.Web
|
||||
{
|
||||
public enum WebApiAccessType
|
||||
{
|
||||
None,
|
||||
AccountManagement,
|
||||
|
||||
/*
|
||||
* Add more access types here as needed.
|
||||
*
|
||||
* Do not change the order of existing types because they are saved to disk,
|
||||
* so changing the order can change access for previously saved keys.
|
||||
*
|
||||
* If you want to add access types for your custom functionality, start with
|
||||
* a higher base value (e.g. 100000) to avoid conflicts with potential future
|
||||
* built-in access types.
|
||||
*/
|
||||
|
||||
NumTypes,
|
||||
}
|
||||
}
|
||||
117
src/MHServerEmu.Core/Network/Web/WebApiKeyManager.cs
Normal file
117
src/MHServerEmu.Core/Network/Web/WebApiKeyManager.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
using System.Text.Json;
|
||||
using MHServerEmu.Core.Helpers;
|
||||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Memory;
|
||||
using MHServerEmu.Core.System;
|
||||
|
||||
namespace MHServerEmu.Core.Network.Web
|
||||
{
|
||||
public enum WebApiKeyVerificationResult
|
||||
{
|
||||
Success,
|
||||
InvalidKey,
|
||||
KeyNotFound,
|
||||
AccessMismatch,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Singleton implementation of <see cref="TokenManager{T}"/> for managing <see cref="WebApiKeyData"/> instances.
|
||||
/// </summary>
|
||||
public class WebApiKeyManager
|
||||
{
|
||||
private static readonly string KeyFilePath = Path.Combine(FileHelper.DataDirectory, "Web", "ApiKeys.json");
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
private readonly TokenManager<WebApiKeyData> _keys = new();
|
||||
|
||||
public static WebApiKeyManager Instance { get; } = new();
|
||||
|
||||
private WebApiKeyManager() { }
|
||||
|
||||
public void LoadKeys()
|
||||
{
|
||||
_keys.Clear();
|
||||
|
||||
if (File.Exists(KeyFilePath) == false)
|
||||
return;
|
||||
|
||||
var keys = FileHelper.DeserializeJson<List<KeyValuePair<string, WebApiKeyData>>>(KeyFilePath);
|
||||
if (keys == null)
|
||||
{
|
||||
Logger.Warn("LoadKeys(): Failed to deserialize web API keys");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var kvp in keys)
|
||||
{
|
||||
string key = kvp.Key;
|
||||
WebApiKeyData keyData = kvp.Value;
|
||||
|
||||
if (_keys.ImportToken(key, keyData) == false)
|
||||
{
|
||||
Logger.Warn($"LoadKeys(): Failed to import web API key [{keyData}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.Info($"Loaded web API key [{keyData}]");
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveKeys()
|
||||
{
|
||||
using var keysHandle = ListPool<KeyValuePair<string, WebApiKeyData>>.Instance.Get(out var keys);
|
||||
_keys.ExportTokens(keys);
|
||||
|
||||
FileHelper.SerializeJson(KeyFilePath, keys, JsonOptions);
|
||||
}
|
||||
|
||||
public string CreateKey(string name, WebApiAccessType access)
|
||||
{
|
||||
string key = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return Logger.WarnReturn(key, $"CreateKey(): Invalid key name '{name}'");
|
||||
|
||||
if (access <= WebApiAccessType.None || access >= WebApiAccessType.NumTypes)
|
||||
return Logger.WarnReturn(key, $"CreateKey(): Invalid access type {access}");
|
||||
|
||||
WebApiKeyData keyData = new(name, access, DateTime.UtcNow);
|
||||
key = _keys.GenerateToken(keyData);
|
||||
|
||||
SaveKeys();
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
public WebApiKeyVerificationResult VerifyKey(string key, WebApiAccessType requiredAccess, out string keyName)
|
||||
{
|
||||
keyName = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return WebApiKeyVerificationResult.InvalidKey;
|
||||
|
||||
if (_keys.TryGetValue(key, out WebApiKeyData keyData) == false)
|
||||
return WebApiKeyVerificationResult.KeyNotFound;
|
||||
|
||||
if (keyData.Access != requiredAccess)
|
||||
return WebApiKeyVerificationResult.AccessMismatch;
|
||||
|
||||
keyName = keyData.Name;
|
||||
return WebApiKeyVerificationResult.Success;
|
||||
}
|
||||
|
||||
private class WebApiKeyData(string name, WebApiAccessType access, DateTime creationTime)
|
||||
{
|
||||
public string Name { get; init; } = name;
|
||||
public WebApiAccessType Access { get; init; } = access;
|
||||
public DateTime CreationTime { get; init; } = creationTime;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} ({Access})";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/MHServerEmu.Core/Network/Web/WebHandler.cs
Normal file
130
src/MHServerEmu.Core/Network/Web/WebHandler.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
using System.Net;
|
||||
using MHServerEmu.Core.Extensions;
|
||||
using MHServerEmu.Core.Logging;
|
||||
|
||||
namespace MHServerEmu.Core.Network.Web
|
||||
{
|
||||
public abstract class WebHandler
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
public virtual WebApiAccessType Access { get => WebApiAccessType.None; }
|
||||
|
||||
public WebService Service { get; private set; }
|
||||
public string LocalPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a reference to the <see cref="WebService"/> this <see cref="WebHandler"/> is registered to.
|
||||
/// </summary>
|
||||
internal void Register(WebService service, string localPath)
|
||||
{
|
||||
Service = service;
|
||||
LocalPath = localPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the reference to the <see cref="WebService"/> this <see cref="WebHandler"/> is currently registered to.
|
||||
/// </summary>
|
||||
internal void Unregister()
|
||||
{
|
||||
Service = null;
|
||||
LocalPath = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="WebRequestContext"/> asynchronously.
|
||||
/// </summary>
|
||||
internal async Task HandleAsync(WebRequestContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Authorize(context) == false)
|
||||
{
|
||||
context.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (context.HttpMethod)
|
||||
{
|
||||
case "GET":
|
||||
await Get(context);
|
||||
break;
|
||||
|
||||
case "POST":
|
||||
await Post(context);
|
||||
break;
|
||||
|
||||
case "DELETE":
|
||||
await Delete(context);
|
||||
break;
|
||||
|
||||
default:
|
||||
await HandleMethodNotAllowed(context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
context.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
Logger.Warn($"Error handling {context}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a GET request asynchronously.
|
||||
/// </summary>
|
||||
protected virtual Task Get(WebRequestContext context)
|
||||
{
|
||||
return HandleMethodNotAllowed(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a POST request asynchronously.
|
||||
/// </summary>
|
||||
protected virtual Task Post(WebRequestContext context)
|
||||
{
|
||||
return HandleMethodNotAllowed(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a DELETE request asynchronously.
|
||||
/// </summary>
|
||||
protected virtual Task Delete(WebRequestContext context)
|
||||
{
|
||||
return HandleMethodNotAllowed(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fallback for unsupported HTTP method requests.
|
||||
/// </summary>
|
||||
private static Task HandleMethodNotAllowed(WebRequestContext context)
|
||||
{
|
||||
Logger.Warn($"Unsupported HTTP method {context.HttpMethod} for local path {context.LocalPath}");
|
||||
context.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool Authorize(WebRequestContext context)
|
||||
{
|
||||
// NOTE: If we decide to add global rate limiting of some kind, this can be done here.
|
||||
|
||||
WebApiAccessType access = Access;
|
||||
if (access == WebApiAccessType.None)
|
||||
return true;
|
||||
|
||||
string ipAddressHandle = context.GetIPAddressHandle();
|
||||
string webApiKey = context.GetBearerToken();
|
||||
|
||||
WebApiKeyVerificationResult result = WebApiKeyManager.Instance.VerifyKey(webApiKey, access, out string keyName);
|
||||
|
||||
if (result != WebApiKeyVerificationResult.Success)
|
||||
{
|
||||
Logger.Warn($"Authorize(): Failed to authorize request to {LocalPath} from {ipAddressHandle} using key [{keyName}], result={result}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.Info($"Authorized request to {LocalPath} from {ipAddressHandle} using key [{keyName}]");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/MHServerEmu.Core/Network/Web/WebRequestContext.cs
Normal file
194
src/MHServerEmu.Core/Network/Web/WebRequestContext.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
using System.Buffers;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
using Google.ProtocolBuffers;
|
||||
|
||||
namespace MHServerEmu.Core.Network.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper for <see cref="HttpListenerContext"/>.
|
||||
/// </summary>
|
||||
public readonly struct WebRequestContext
|
||||
{
|
||||
private readonly HttpListenerRequest _httpRequest;
|
||||
private readonly HttpListenerResponse _httpResponse;
|
||||
|
||||
public string UserAgent { get => _httpRequest.UserAgent; }
|
||||
public string LocalPath { get => _httpRequest.Url.LocalPath; }
|
||||
public string HttpMethod { get => _httpRequest.HttpMethod; }
|
||||
public string XForwardedFor { get => _httpRequest.Headers["X-Forwarded-For"]; }
|
||||
public string Authorization { get => _httpRequest.Headers["Authorization"]; }
|
||||
|
||||
public bool IsGameClientRequest { get => UserAgent.Equals("Secret Identity Studios Http Client", StringComparison.InvariantCulture); }
|
||||
|
||||
public int StatusCode { get => _httpResponse.StatusCode; set => _httpResponse.StatusCode = value; }
|
||||
|
||||
public WebRequestContext(HttpListenerContext httpContext)
|
||||
{
|
||||
_httpRequest = httpContext.Request;
|
||||
_httpResponse = httpContext.Response;
|
||||
|
||||
_httpResponse.StatusCode = 200;
|
||||
_httpResponse.KeepAlive = false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{HttpMethod} {LocalPath}";
|
||||
}
|
||||
|
||||
public string GetIPAddress()
|
||||
{
|
||||
string forwardedFor = XForwardedFor;
|
||||
if (string.IsNullOrWhiteSpace(forwardedFor) == false)
|
||||
return forwardedFor;
|
||||
|
||||
return _httpRequest.RemoteEndPoint.Address.ToString();
|
||||
}
|
||||
|
||||
public string GetBearerToken()
|
||||
{
|
||||
string authorization = Authorization;
|
||||
if (string.IsNullOrWhiteSpace(authorization))
|
||||
return null;
|
||||
|
||||
string[] data = authorization.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (data == null || data.Length < 2)
|
||||
return null;
|
||||
|
||||
if (data[0].Equals("Bearer", StringComparison.OrdinalIgnoreCase) == false)
|
||||
return null;
|
||||
|
||||
return data[1];
|
||||
}
|
||||
|
||||
public void Redirect(string url)
|
||||
{
|
||||
_httpResponse.Redirect(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads the request input stream as a UTF-8 string.
|
||||
/// </summary>
|
||||
public async Task<string> ReadUtf8StringAsync()
|
||||
{
|
||||
const long MaxLength = 1024 * 16;
|
||||
|
||||
int length = (int)_httpRequest.ContentLength64;
|
||||
if (length < 0 || length > MaxLength)
|
||||
throw new InternalBufferOverflowException();
|
||||
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(length);
|
||||
|
||||
try
|
||||
{
|
||||
await _httpRequest.InputStream.ReadAsync(buffer.AsMemory(0, length));
|
||||
return Encoding.UTF8.GetString(buffer, 0, length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads the request input stream as <typeparamref name="T"/> serialized using JSON.
|
||||
/// </summary>
|
||||
public async Task<T> ReadJsonAsync<T>()
|
||||
{
|
||||
return await JsonSerializer.DeserializeAsync<T>(_httpRequest.InputStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads the request input stream as a <see cref="NameValueCollection"/>.
|
||||
/// </summary>
|
||||
public async Task<NameValueCollection> ReadQueryStringAsync()
|
||||
{
|
||||
string queryString = await ReadUtf8StringAsync();
|
||||
return HttpUtility.ParseQueryString(queryString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the request input stream as an <see cref="IMessage"/> of protocol <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
public IMessage ReadProtobuf<T>() where T: Enum
|
||||
{
|
||||
MessageBuffer messageBuffer = new(_httpRequest.InputStream);
|
||||
return messageBuffer.Deserialize<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously responds to the request with the provided payload.
|
||||
/// </summary>
|
||||
public async Task SendAsync(byte[] payload, string contentType)
|
||||
{
|
||||
_httpResponse.ContentType = contentType;
|
||||
_httpResponse.ContentLength64 = payload.Length;
|
||||
|
||||
await _httpResponse.OutputStream.WriteAsync(payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously responds to the request with the provided <see cref="string"/> encoded as UTF-8.
|
||||
/// </summary>
|
||||
public async Task SendAsync(string message, string contentType = "text/plain")
|
||||
{
|
||||
int maxByteCount = Encoding.UTF8.GetMaxByteCount(message.Length);
|
||||
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(maxByteCount);
|
||||
|
||||
try
|
||||
{
|
||||
int byteCount = Encoding.UTF8.GetBytes(message, 0, message.Length, buffer, 0);
|
||||
|
||||
_httpResponse.ContentType = contentType;
|
||||
_httpResponse.ContentLength64 = byteCount;
|
||||
|
||||
await _httpResponse.OutputStream.WriteAsync(buffer.AsMemory(0, byteCount));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously responds to the request with the provided <see cref="IMessage"/>.
|
||||
/// </summary>
|
||||
public async Task SendAsync(IMessage message)
|
||||
{
|
||||
MessagePackageOut payload = new(message);
|
||||
int size = payload.GetSerializedSize();
|
||||
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
|
||||
|
||||
try
|
||||
{
|
||||
CodedOutputStream cos = CodedOutputStream.CreateInstance(buffer);
|
||||
payload.WriteTo(cos);
|
||||
cos.Flush();
|
||||
|
||||
_httpResponse.ContentType = "application/octet-stream";
|
||||
_httpResponse.ContentLength64 = size;
|
||||
|
||||
await _httpResponse.OutputStream.WriteAsync(buffer.AsMemory(0, size));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously responds to the request with the provided <typeparamref name="T"/> instance serialized to JSON.
|
||||
/// </summary>
|
||||
public async Task SendJsonAsync<T>(T @object)
|
||||
{
|
||||
_httpResponse.ContentType = "application/json";
|
||||
await JsonSerializer.SerializeAsync(_httpResponse.OutputStream, @object);
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/MHServerEmu.Core/Network/Web/WebService.cs
Normal file
158
src/MHServerEmu.Core/Network/Web/WebService.cs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using MHServerEmu.Core.Logging;
|
||||
|
||||
namespace MHServerEmu.Core.Network.Web
|
||||
{
|
||||
public class WebService
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
private readonly Dictionary<string, WebHandler> _handlers = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private HttpListener _listener;
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
public WebServiceSettings Settings { get; }
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public int HandlerCount { get => _handlers.Count; }
|
||||
public int HandledRequests { get; private set; }
|
||||
|
||||
public WebService(WebServiceSettings settings)
|
||||
{
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Settings.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the web service. Returns <see langword="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool Start()
|
||||
{
|
||||
if (IsRunning)
|
||||
return false;
|
||||
|
||||
Debug.Assert(_listener == null);
|
||||
Debug.Assert(_cts == null);
|
||||
|
||||
string url = Settings.ListenUrl;
|
||||
|
||||
_listener = new();
|
||||
_listener.Prefixes.Add(url);
|
||||
_listener.Start();
|
||||
|
||||
_cts = new();
|
||||
Task.Run(HandleRequestsAsync);
|
||||
|
||||
IsRunning = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the currently running REST service. Returns <see langword="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool Stop()
|
||||
{
|
||||
if (IsRunning == false)
|
||||
return false;
|
||||
|
||||
Debug.Assert(_listener != null);
|
||||
Debug.Assert(_cts != null);
|
||||
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
_cts = null;
|
||||
|
||||
_listener.Stop();
|
||||
_listener = null;
|
||||
|
||||
IsRunning = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the currently registered <see cref="WebHandler"/> for the specified local path if available.
|
||||
/// Returns the fallback handler if no handler is registered for the local path, which may be <see langword="null"/>.
|
||||
/// </summary>
|
||||
public WebHandler GetHandler(string localPath)
|
||||
{
|
||||
if (_handlers.TryGetValue(localPath, out WebHandler handler) == false)
|
||||
return Settings.FallbackHandler;
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the provided <see cref="WebHandler"/> for the specified local path.
|
||||
/// Returns <see langword="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool RegisterHandler(string localPath, WebHandler handler)
|
||||
{
|
||||
bool added = _handlers.TryAdd(localPath, handler);
|
||||
|
||||
if (added)
|
||||
handler.Register(this, localPath);
|
||||
else
|
||||
Logger.Warn($"RegisterHandler(): Local path {localPath} already has a registered handler");
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removed the currently registered <see cref="WebHandler"/> for the specified local path.
|
||||
/// Returns <see langword="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool RemoveHandler(string localPath)
|
||||
{
|
||||
bool removed = _handlers.Remove(localPath, out WebHandler handler);
|
||||
|
||||
if (removed)
|
||||
handler.Unregister();
|
||||
else
|
||||
Logger.Warn($"RemoveHandler(): No handler is registered for local path {localPath}");
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles incoming requests asynchronously.
|
||||
/// </summary>
|
||||
private async Task HandleRequestsAsync()
|
||||
{
|
||||
Logger.Info($"{this} is listening on {Settings.ListenUrl}...");
|
||||
|
||||
while (_cts.IsCancellationRequested == false)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpListenerContext httpContext = await _listener.GetContextAsync().WaitAsync(_cts.Token);
|
||||
WebRequestContext requestContext = new(httpContext);
|
||||
|
||||
// This may be either a registered handler or a fallback handler.
|
||||
WebHandler handler = GetHandler(requestContext.LocalPath);
|
||||
await handler?.HandleAsync(requestContext);
|
||||
|
||||
httpContext.Response.Close();
|
||||
|
||||
HandledRequests++;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// NOTE: HandleRequest() should catch and handle exceptions when processing requests.
|
||||
// If we got to this part, something must be wrong with the listener.
|
||||
Logger.Error($"HandleRequestAsync(): {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/MHServerEmu.Core/Network/Web/WebServiceSettings.cs
Normal file
9
src/MHServerEmu.Core/Network/Web/WebServiceSettings.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace MHServerEmu.Core.Network.Web
|
||||
{
|
||||
public class WebServiceSettings
|
||||
{
|
||||
public string Name { get; init; }
|
||||
public string ListenUrl { get; init; }
|
||||
public WebHandler FallbackHandler { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Diagnostics;
|
||||
using MHServerEmu.Core.System.Time;
|
||||
|
||||
namespace MHServerEmu.Core.System
|
||||
namespace MHServerEmu.Core.RateLimiting
|
||||
{
|
||||
public class TimeLeakyBucketCollection<TKey>
|
||||
{
|
||||
|
|
@ -25,11 +25,11 @@ namespace MHServerEmu.Core.System
|
|||
_dict.TryGetValue(key, out TimeSpan time);
|
||||
|
||||
TimeSpan currentTime = Clock.ElapsedTime;
|
||||
if ((currentTime - time) > TimeSpan.Zero)
|
||||
if (currentTime - time > TimeSpan.Zero)
|
||||
time = currentTime;
|
||||
|
||||
TimeSpan newTime = time + _cost;
|
||||
if ((newTime - currentTime) >= _maxCost)
|
||||
if (newTime - currentTime >= _maxCost)
|
||||
{
|
||||
_dict[key] = time;
|
||||
return false;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Diagnostics;
|
||||
using MHServerEmu.Core.System.Time;
|
||||
|
||||
namespace MHServerEmu.Core.System
|
||||
namespace MHServerEmu.Core.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// A rate limiter based on the token bucket algorithm.
|
||||
|
|
@ -26,6 +26,7 @@ namespace MHServerEmu.Core.System
|
|||
_maxTokens = maxTokens;
|
||||
|
||||
_currentTokens = _maxTokens;
|
||||
_lastRefillTime = Clock.ElapsedTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -51,10 +52,10 @@ namespace MHServerEmu.Core.System
|
|||
TimeSpan currentTime = Clock.ElapsedTime;
|
||||
TimeSpan elapsed = currentTime - _lastRefillTime;
|
||||
|
||||
int tokensGained = (int)(elapsed.Ticks / _ticksPerToken);
|
||||
long tokensGained = elapsed.Ticks / _ticksPerToken;
|
||||
if (tokensGained > 0)
|
||||
{
|
||||
_currentTokens = Math.Min(_currentTokens + tokensGained, _maxTokens);
|
||||
_currentTokens = (int)Math.Clamp(_currentTokens + tokensGained, 0, _maxTokens);
|
||||
_lastRefillTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,38 +19,48 @@ namespace MHServerEmu.Core.Serialization
|
|||
Disk = 4 // Server <-> File
|
||||
}
|
||||
|
||||
// We are most likely not going to have as much versioning as the original game,
|
||||
// so we will use ArchiveVersion for all archive versioning. At some point when
|
||||
// things get more stable we may want to clear this and force a wipe of everything.
|
||||
public enum ArchiveVersion : uint
|
||||
{
|
||||
Invalid = 0,
|
||||
Initial = 1,
|
||||
AddedMissions = 2,
|
||||
AddedVendorPurchaseData = 3,
|
||||
ImplementedConditionPersistence = 4,
|
||||
ImplementedLoginRewards = 5,
|
||||
ImplementedMapDiscoveryDataPersistence = 6,
|
||||
AddedRegionProtoRefToMapDiscoveryData = 7,
|
||||
// Versions 1-8 were used in the 0.x branch, so we start at 9 here.
|
||||
Initial = 9,
|
||||
|
||||
// Update the current version if you add any <---------
|
||||
Current = AddedRegionProtoRefToMapDiscoveryData
|
||||
Current = Initial
|
||||
}
|
||||
|
||||
public enum GameBuildNumber : uint
|
||||
{
|
||||
Invalid = 0,
|
||||
|
||||
// Changelist is the most consistent single number we have to identify different builds of the game, so use it for persistent archives.
|
||||
// Add more changelist numbers here for any other versions of the game we are going to support.
|
||||
_1_10_0_69 = 324,
|
||||
_1_10_0_643 = 16688,
|
||||
_1_48_0_1618 = 380454,
|
||||
_1_48_0_1712 = 391562,
|
||||
_1_52_0_1700 = 479899,
|
||||
_1_53_0_203 = 493640,
|
||||
|
||||
Current = _1_52_0_1700
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of the custom Gazillion serialization archive format.
|
||||
/// </summary>
|
||||
public class Archive : IDisposable
|
||||
public sealed class Archive : IDisposable
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
// Reuse the same buffers for all archives on the same thread. In practice this means one buffer instance of each type per game.
|
||||
// Reuse the same buffers for all archives on the same game thread.
|
||||
[ThreadStatic]
|
||||
private static byte[] ReadBuffer;
|
||||
[ThreadStatic]
|
||||
private static byte[] WriteBuffer;
|
||||
[ThreadStatic]
|
||||
private static MemoryStream SharedAutoBuffer;
|
||||
[ThreadStatic]
|
||||
private static byte[] CodedOutputStreamBuffer;
|
||||
|
||||
private readonly MemoryStream _bufferStream; // MemoryStream replaces AutoBuffer from the original implementation
|
||||
private readonly MemoryStream _buffer; // MemoryStream replaces StreamAutoBuffer from the original implementation
|
||||
|
||||
// C# coded stream implementation is buffered, so we have to use the same stream for the whole archive
|
||||
private readonly CodedOutputStream _cos;
|
||||
|
|
@ -108,20 +118,15 @@ namespace MHServerEmu.Core.Serialization
|
|||
if ((serializeType == ArchiveSerializeType.Replication || serializeType == ArchiveSerializeType.Database) == false)
|
||||
throw new NotImplementedException($"Unsupported archive serialize type {serializeType}.");
|
||||
|
||||
// Initialize new buffers if this is being called for the first time on this thread.
|
||||
if (SharedAutoBuffer == null)
|
||||
{
|
||||
SharedAutoBuffer = new(1024);
|
||||
CodedOutputStreamBuffer = new byte[32]; // We flush after every value, so we can use very small buffer sizes (default is 4096).
|
||||
}
|
||||
InitializeBuffers();
|
||||
|
||||
// Reuse the same stream for all packing archives
|
||||
_bufferStream = SharedAutoBuffer;
|
||||
if (_bufferStream.Length > 0)
|
||||
_bufferStream.SetLength(0);
|
||||
_buffer = SharedAutoBuffer;
|
||||
if (_buffer.Length > 0)
|
||||
_buffer.SetLength(0);
|
||||
|
||||
// Use reflection hackery to reuse the same buffer for all coded output streams, see ProtobufHelper for details.
|
||||
_cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(_bufferStream, CodedOutputStreamBuffer);
|
||||
_cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(_buffer, WriteBuffer);
|
||||
|
||||
SerializeType = serializeType;
|
||||
ReplicationPolicy = replicationPolicy;
|
||||
|
|
@ -139,8 +144,10 @@ namespace MHServerEmu.Core.Serialization
|
|||
if ((serializeType == ArchiveSerializeType.Replication || serializeType == ArchiveSerializeType.Database) == false)
|
||||
throw new NotImplementedException($"Unsupported archive serialize type {serializeType}.");
|
||||
|
||||
_bufferStream = new(buffer);
|
||||
_cis = CodedInputStream.CreateInstance(_bufferStream);
|
||||
InitializeBuffers();
|
||||
|
||||
_buffer = new(buffer);
|
||||
_cis = CodedInputStream.CreateInstance(_buffer, ReadBuffer);
|
||||
|
||||
SerializeType = serializeType;
|
||||
IsPacking = false;
|
||||
|
|
@ -157,13 +164,34 @@ namespace MHServerEmu.Core.Serialization
|
|||
// We use ByteString.Unsafe here to avoid copying data one extra time (ByteString -> Stream instead of ByteString -> Buffer -> Stream).
|
||||
}
|
||||
|
||||
private static void InitializeBuffers()
|
||||
{
|
||||
// Initialize new buffers if this is being called for the first time on this thread.
|
||||
// NOTE: This approach prevents us from doing recursive writing of multiple archives on the same thread.
|
||||
// In practice this is not a limitation that affects us, but it is something to keep in mind.
|
||||
if (ReadBuffer == null || WriteBuffer == null || SharedAutoBuffer == null)
|
||||
{
|
||||
ReadBuffer = new byte[4096];
|
||||
WriteBuffer = new byte[32]; // We flush after every value, so we can use very small buffer sizes for output (default is 4096).
|
||||
SharedAutoBuffer = new(65536);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_buffer.SetLength(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="MemoryStream"/> instance that acts as the AutoBuffer for this <see cref="Archive"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AutoBuffer is the name of the data structure that backs archives in the client.
|
||||
/// </remarks>
|
||||
public MemoryStream AccessAutoBuffer() => _bufferStream;
|
||||
public MemoryStream AccessAutoBuffer()
|
||||
{
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the underlying <see cref="MemoryStream"/> to <see cref="ByteString"/>.
|
||||
|
|
@ -171,7 +199,13 @@ namespace MHServerEmu.Core.Serialization
|
|||
public ByteString ToByteString()
|
||||
{
|
||||
// We use ByteString.Unsafe here to avoid copying data one extra time (Stream -> ByteString instead of Stream -> Buffer -> ByteString).
|
||||
return ByteString.Unsafe.FromBytes(_bufferStream.ToArray());
|
||||
return ByteString.Unsafe.FromBytes(_buffer.ToArray());
|
||||
}
|
||||
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
byte[] buffer = _buffer.GetBuffer();
|
||||
return buffer.AsSpan(0, (int)CurrentOffset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -183,8 +217,14 @@ namespace MHServerEmu.Core.Serialization
|
|||
|
||||
if (IsPersistent)
|
||||
{
|
||||
// Write archive size placeholder that will be updated via UpdateSizeInArchive() when other data is written.
|
||||
WriteUnencodedStream(0u);
|
||||
|
||||
uint version = (uint)Version;
|
||||
success &= Transfer(ref version);
|
||||
|
||||
uint gameBuildNumber = (uint)GameBuildNumber.Current;
|
||||
success &= Transfer(ref gameBuildNumber);
|
||||
}
|
||||
else if (IsReplication)
|
||||
{
|
||||
|
|
@ -204,9 +244,24 @@ namespace MHServerEmu.Core.Serialization
|
|||
|
||||
if (IsPersistent)
|
||||
{
|
||||
uint sizeUsed = 0;
|
||||
success &= ReadUnencodedStream(ref sizeUsed);
|
||||
|
||||
if (sizeUsed != _buffer.Length)
|
||||
{
|
||||
SetError("Buffer size mismatch!");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint version = 0;
|
||||
success &= Transfer(ref version);
|
||||
Version = (ArchiveVersion)version;
|
||||
|
||||
// For now just log a warning if there is a game build mismatch, in the future we can use this for migration between versions.
|
||||
uint gameBuildNumber = 0;
|
||||
success &= Transfer(ref gameBuildNumber);
|
||||
if (gameBuildNumber != (uint)GameBuildNumber.Current)
|
||||
Logger.Warn($"Game build number mismatch: expected {(uint)GameBuildNumber.Current}, got {gameBuildNumber}");
|
||||
}
|
||||
else if (IsReplication)
|
||||
{
|
||||
|
|
@ -253,7 +308,7 @@ namespace MHServerEmu.Core.Serialization
|
|||
}
|
||||
else
|
||||
{
|
||||
_bufferStream.WriteByteAt(_lastBitEncodedOffset, bitBuffer);
|
||||
_buffer.WriteByteAt(_lastBitEncodedOffset, bitBuffer);
|
||||
if (numEncodedBits >= 5)
|
||||
_lastBitEncodedOffset = 0;
|
||||
}
|
||||
|
|
@ -606,7 +661,8 @@ namespace MHServerEmu.Core.Serialization
|
|||
|
||||
if (IsPacking)
|
||||
{
|
||||
return WriteVarint(ioData);
|
||||
WriteVarint(ioData);
|
||||
UpdateSizeInArchive();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -633,7 +689,8 @@ namespace MHServerEmu.Core.Serialization
|
|||
|
||||
if (IsPacking)
|
||||
{
|
||||
return WriteVarint(ioData);
|
||||
WriteVarint(ioData);
|
||||
UpdateSizeInArchive();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -675,7 +732,7 @@ namespace MHServerEmu.Core.Serialization
|
|||
/// </summary>
|
||||
private bool StartSizeChecking(ref long startPosition, ref uint size)
|
||||
{
|
||||
if (IsPersistent == false || Version < ArchiveVersion.AddedMissions)
|
||||
if (IsPersistent == false)
|
||||
return true;
|
||||
|
||||
// NOTE: COS/CIS are buffered, so we need to use their position, and not the one from the underlying stream.
|
||||
|
|
@ -701,7 +758,7 @@ namespace MHServerEmu.Core.Serialization
|
|||
/// </summary>
|
||||
private bool EndSizeChecking(ref long startPosition, ref uint size, bool skip)
|
||||
{
|
||||
if (IsPersistent == false || Version < ArchiveVersion.AddedMissions)
|
||||
if (IsPersistent == false)
|
||||
return true;
|
||||
|
||||
if (IsPacking)
|
||||
|
|
@ -729,6 +786,40 @@ namespace MHServerEmu.Core.Serialization
|
|||
return true;
|
||||
}
|
||||
|
||||
private bool UpdateSizeInArchive()
|
||||
{
|
||||
if (IsPersistent == false)
|
||||
return false;
|
||||
|
||||
if (IsPacking == false)
|
||||
{
|
||||
SetError("Cant use on unpack!");
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<uint> sizeToken = GetSizeTokenAtOffset(0);
|
||||
if (sizeToken.Length == 0)
|
||||
{
|
||||
SetError("Error accessing size in the buffer!");
|
||||
return false;
|
||||
}
|
||||
|
||||
sizeToken[0] = (uint)CurrentOffset;
|
||||
return true;
|
||||
}
|
||||
|
||||
private Span<uint> GetSizeTokenAtOffset(uint offset)
|
||||
{
|
||||
if (CurrentOffset < offset + sizeof(uint))
|
||||
{
|
||||
SetError("Failed writing size in use!");
|
||||
return default;
|
||||
}
|
||||
|
||||
Span<byte> sizeToken = new(_buffer.GetBuffer(), (int)offset, sizeof(uint));
|
||||
return MemoryMarshal.Cast<byte, uint>(sizeToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream IO
|
||||
|
|
@ -740,6 +831,9 @@ namespace MHServerEmu.Core.Serialization
|
|||
{
|
||||
_cos.WriteRawByte(value);
|
||||
_cos.Flush();
|
||||
|
||||
UpdateSizeInArchive();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -770,6 +864,9 @@ namespace MHServerEmu.Core.Serialization
|
|||
_cos.WriteRawByte(@byte);
|
||||
|
||||
_cos.Flush();
|
||||
|
||||
UpdateSizeInArchive();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -806,7 +903,7 @@ namespace MHServerEmu.Core.Serialization
|
|||
// NOTE: PropertyCollection::serializeWithDefault() manipulates the archive buffer directly. First it allocates 4 bytes
|
||||
// for the number of properties, than it writes all the properties, and then it goes back and updates the number.
|
||||
// NOTE2: Persistent archives also do this for all ISerialize objects, except it writes the number of bytes written.
|
||||
return _bufferStream.WriteUInt32At(position, value);
|
||||
return _buffer.WriteUInt32At(position, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -921,7 +1018,7 @@ namespace MHServerEmu.Core.Serialization
|
|||
{
|
||||
if (_lastBitEncodedOffset == 0) return null;
|
||||
|
||||
if (_bufferStream.ReadByteAt(_lastBitEncodedOffset, out byte lastBitEncoded) == false)
|
||||
if (_buffer.ReadByteAt(_lastBitEncodedOffset, out byte lastBitEncoded) == false)
|
||||
{
|
||||
SetError("Failed getting last bit encoded!");
|
||||
return null;
|
||||
|
|
@ -978,31 +1075,5 @@ namespace MHServerEmu.Core.Serialization
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Not sure if we even still need IDisposable for archives with reusable streams,
|
||||
// we can just rely on doing cleanup after the previous use in the constructor.
|
||||
_bufferStream.SetLength(0);
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
src/MHServerEmu.Core/Serialization/ICodedOutputStreamEx.cs
Normal file
18
src/MHServerEmu.Core/Serialization/ICodedOutputStreamEx.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using Google.ProtocolBuffers;
|
||||
|
||||
namespace MHServerEmu.Core.Serialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Extended version of <see cref="ICodedOutputStream"/> that exposes additional low level writing functionality.
|
||||
/// </summary>
|
||||
public interface ICodedOutputStreamEx : ICodedOutputStream
|
||||
{
|
||||
void WriteRawVarint32(uint value);
|
||||
|
||||
void WriteRawVarint64(ulong value);
|
||||
|
||||
void WriteRawByte(byte value);
|
||||
|
||||
void WriteRawBytes(byte[] value);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,459 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Google.ProtocolBuffers;
|
||||
using Google.ProtocolBuffers.Descriptors;
|
||||
using MHServerEmu.Core.Helpers;
|
||||
|
||||
namespace MHServerEmu.Core.Serialization
|
||||
{
|
||||
/// <summary>
|
||||
/// A more memory efficient version of <see cref="CodedOutputStream"/>.
|
||||
/// </summary>
|
||||
public sealed class RecyclableCodedOutputStream : ICodedOutputStreamEx, IDisposable
|
||||
{
|
||||
private static readonly ConcurrentBag<RecyclableCodedOutputStream> Instances = new();
|
||||
|
||||
private readonly byte[] _primaryBuffer = new byte[CodedOutputStream.DefaultBufferSize];
|
||||
private readonly byte[] _floatBuffer = new byte[sizeof(float)];
|
||||
|
||||
private CodedOutputStream _cos;
|
||||
|
||||
private RecyclableCodedOutputStream() { }
|
||||
|
||||
private void Initialize(Stream stream)
|
||||
{
|
||||
_cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(stream, _primaryBuffer);
|
||||
}
|
||||
|
||||
public static RecyclableCodedOutputStream CreateInstance(Stream stream)
|
||||
{
|
||||
if (Instances.TryTake(out RecyclableCodedOutputStream cos) == false)
|
||||
cos = new();
|
||||
|
||||
cos.Initialize(stream);
|
||||
return cos;
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_cos != null)
|
||||
{
|
||||
_cos.Flush();
|
||||
_cos = null;
|
||||
}
|
||||
|
||||
Instances.Add(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ICodedOutputStream
|
||||
|
||||
// For most of this we just pass everything to the default implementation.
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Flush()
|
||||
{
|
||||
_cos.Flush();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteArray(FieldType fieldType, int fieldNumber, string fieldName, IEnumerable list)
|
||||
{
|
||||
_cos.WriteArray(fieldType, fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteBool(int fieldNumber, string fieldName, bool value)
|
||||
{
|
||||
_cos.WriteBool(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteBoolArray(int fieldNumber, string fieldName, IEnumerable<bool> list)
|
||||
{
|
||||
_cos.WriteBoolArray(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteBytes(int fieldNumber, string fieldName, ByteString value)
|
||||
{
|
||||
_cos.WriteBytes(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteBytesArray(int fieldNumber, string fieldName, IEnumerable<ByteString> list)
|
||||
{
|
||||
_cos.WriteBytesArray(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDouble(int fieldNumber, string fieldName, double value)
|
||||
{
|
||||
_cos.WriteDouble(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDoubleArray(int fieldNumber, string fieldName, IEnumerable<double> list)
|
||||
{
|
||||
_cos.WriteDoubleArray(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteEnum(int fieldNumber, string fieldName, int value, object rawValue)
|
||||
{
|
||||
_cos.WriteEnum(fieldNumber, fieldName, value, rawValue);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteEnumArray<T>(int fieldNumber, string fieldName, IEnumerable<T> list) where T : struct, IComparable, IFormattable
|
||||
{
|
||||
_cos.WriteEnumArray(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteField(FieldType fieldType, int fieldNumber, string fieldName, object value)
|
||||
{
|
||||
_cos.WriteField(fieldType, fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFixed32(int fieldNumber, string fieldName, uint value)
|
||||
{
|
||||
_cos.WriteFixed32(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFixed32Array(int fieldNumber, string fieldName, IEnumerable<uint> list)
|
||||
{
|
||||
_cos.WriteFixed32Array(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFixed64(int fieldNumber, string fieldName, ulong value)
|
||||
{
|
||||
_cos.WriteFixed64(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFixed64Array(int fieldNumber, string fieldName, IEnumerable<ulong> list)
|
||||
{
|
||||
_cos.WriteFixed64Array(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFloat(int fieldNumber, string fieldName, float value)
|
||||
{
|
||||
//_cos.WriteFloat(fieldNumber, fieldName, value);
|
||||
|
||||
_cos.WriteTag(fieldNumber, WireFormat.WireType.Fixed32);
|
||||
|
||||
MemoryMarshal.Cast<byte, float>(_floatBuffer)[0] = value;
|
||||
_cos.WriteRawBytes(_floatBuffer, 0, 4);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFloatArray(int fieldNumber, string fieldName, IEnumerable<float> list)
|
||||
{
|
||||
_cos.WriteFloatArray(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteGroup(int fieldNumber, string fieldName, IMessageLite value)
|
||||
{
|
||||
_cos.WriteGroup(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteGroupArray<T>(int fieldNumber, string fieldName, IEnumerable<T> list) where T : IMessageLite
|
||||
{
|
||||
_cos.WriteGroupArray(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteInt32(int fieldNumber, string fieldName, int value)
|
||||
{
|
||||
_cos.WriteInt32(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteInt32Array(int fieldNumber, string fieldName, IEnumerable<int> list)
|
||||
{
|
||||
_cos.WriteInt32Array(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteInt64(int fieldNumber, string fieldName, long value)
|
||||
{
|
||||
_cos.WriteInt64(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteInt64Array(int fieldNumber, string fieldName, IEnumerable<long> list)
|
||||
{
|
||||
_cos.WriteInt64Array(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteMessage(int fieldNumber, string fieldName, IMessageLite value)
|
||||
{
|
||||
_cos.WriteMessage(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteMessageArray<T>(int fieldNumber, string fieldName, IEnumerable<T> list) where T : IMessageLite
|
||||
{
|
||||
_cos.WriteMessageArray(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteMessageEnd()
|
||||
{
|
||||
_cos.Flush();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteMessageSetExtension(int fieldNumber, string fieldName, IMessageLite value)
|
||||
{
|
||||
_cos.WriteMessageSetExtension(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteMessageSetExtension(int fieldNumber, string fieldName, ByteString value)
|
||||
{
|
||||
_cos.WriteMessageSetExtension(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteMessageStart()
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedArray(FieldType fieldType, int fieldNumber, string fieldName, IEnumerable list)
|
||||
{
|
||||
_cos.WritePackedArray(fieldType, fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedBoolArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<bool> list)
|
||||
{
|
||||
_cos.WritePackedBoolArray(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedDoubleArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<double> list)
|
||||
{
|
||||
_cos.WritePackedDoubleArray(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedEnumArray<T>(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<T> list) where T : struct, IComparable, IFormattable
|
||||
{
|
||||
_cos.WritePackedEnumArray(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedFixed32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<uint> list)
|
||||
{
|
||||
_cos.WritePackedFixed32Array(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedFixed64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<ulong> list)
|
||||
{
|
||||
_cos.WritePackedFixed64Array(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedFloatArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<float> list)
|
||||
{
|
||||
_cos.WritePackedFloatArray(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<int> list)
|
||||
{
|
||||
_cos.WritePackedInt32Array(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<long> list)
|
||||
{
|
||||
_cos.WritePackedInt64Array(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedSFixed32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<int> list)
|
||||
{
|
||||
_cos.WritePackedSFixed32Array(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedSFixed64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<long> list)
|
||||
{
|
||||
_cos.WritePackedSFixed64Array(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedSInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<int> list)
|
||||
{
|
||||
_cos.WritePackedSInt32Array(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedSInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<long> list)
|
||||
{
|
||||
_cos.WritePackedSInt64Array(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedUInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<uint> list)
|
||||
{
|
||||
_cos.WritePackedUInt32Array(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePackedUInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<ulong> list)
|
||||
{
|
||||
_cos.WritePackedUInt64Array(fieldNumber, fieldName, calculatedSize, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteSFixed32(int fieldNumber, string fieldName, int value)
|
||||
{
|
||||
_cos.WriteSFixed32(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteSFixed32Array(int fieldNumber, string fieldName, IEnumerable<int> list)
|
||||
{
|
||||
_cos.WriteSFixed32Array(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteSFixed64(int fieldNumber, string fieldName, long value)
|
||||
{
|
||||
_cos.WriteSFixed64(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteSFixed64Array(int fieldNumber, string fieldName, IEnumerable<long> list)
|
||||
{
|
||||
_cos.WriteSFixed64Array(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteSInt32(int fieldNumber, string fieldName, int value)
|
||||
{
|
||||
_cos.WriteSInt32(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteSInt32Array(int fieldNumber, string fieldName, IEnumerable<int> list)
|
||||
{
|
||||
_cos.WriteSInt32Array(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteSInt64(int fieldNumber, string fieldName, long value)
|
||||
{
|
||||
_cos.WriteSInt64(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteSInt64Array(int fieldNumber, string fieldName, IEnumerable<long> list)
|
||||
{
|
||||
_cos.WriteSInt64Array(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteString(int fieldNumber, string fieldName, string value)
|
||||
{
|
||||
_cos.WriteString(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteStringArray(int fieldNumber, string fieldName, IEnumerable<string> list)
|
||||
{
|
||||
_cos.WriteStringArray(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteUInt32(int fieldNumber, string fieldName, uint value)
|
||||
{
|
||||
_cos.WriteUInt32(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteUInt32Array(int fieldNumber, string fieldName, IEnumerable<uint> list)
|
||||
{
|
||||
_cos.WriteUInt32Array(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteUInt64(int fieldNumber, string fieldName, ulong value)
|
||||
{
|
||||
_cos.WriteUInt64(fieldNumber, fieldName, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteUInt64Array(int fieldNumber, string fieldName, IEnumerable<ulong> list)
|
||||
{
|
||||
_cos.WriteUInt64Array(fieldNumber, fieldName, list);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteUnknownBytes(int fieldNumber, ByteString value)
|
||||
{
|
||||
_cos.WriteUnknownBytes(fieldNumber, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteUnknownField(int fieldNumber, WireFormat.WireType wireType, ulong value)
|
||||
{
|
||||
_cos.WriteUnknownField(fieldNumber, wireType, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Obsolete]
|
||||
public void WriteUnknownGroup(int fieldNumber, IMessageLite value)
|
||||
{
|
||||
_cos.WriteUnknownGroup(fieldNumber, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ICodedOutputStreamEx
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteRawVarint32(uint value)
|
||||
{
|
||||
_cos.WriteRawVarint32(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteRawVarint64(ulong value)
|
||||
{
|
||||
_cos.WriteRawVarint64(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteRawByte(byte value)
|
||||
{
|
||||
_cos.WriteRawByte(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteRawBytes(byte[] value)
|
||||
{
|
||||
_cos.WriteRawBytes(value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
namespace MHServerEmu.Core.System.Random
|
||||
using MHServerEmu.Core.System.Time;
|
||||
|
||||
namespace MHServerEmu.Core.System.Random
|
||||
{
|
||||
// More info on MWC random: https://en.wikipedia.org/wiki/Multiply-with-carry_pseudorandom_number_generator
|
||||
public class RandMwc
|
||||
|
|
@ -8,7 +10,7 @@
|
|||
|
||||
public RandMwc(uint seed)
|
||||
{
|
||||
SetSeed(seed == 0 ? (uint)DateTime.Now.Ticks : seed);
|
||||
SetSeed(seed == 0 ? (uint)Clock.UtcNowPrecise.Ticks : seed);
|
||||
}
|
||||
|
||||
public void SetSeed(uint seed)
|
||||
|
|
|
|||
71
src/MHServerEmu.Core/System/Time/ServiceEventScheduler.cs
Normal file
71
src/MHServerEmu.Core/System/Time/ServiceEventScheduler.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Memory;
|
||||
|
||||
namespace MHServerEmu.Core.System.Time
|
||||
{
|
||||
/// <summary>
|
||||
/// A timed event scheduler implementation intended to be used in game services where performance and precise timing is not as important.
|
||||
/// </summary>
|
||||
public class ServiceEventScheduler<THandle, TEventData>
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
private readonly Dictionary<THandle, ServiceEvent> _events = new();
|
||||
|
||||
public ServiceEventScheduler()
|
||||
{
|
||||
}
|
||||
|
||||
public void TriggerEvents()
|
||||
{
|
||||
if (_events.Count == 0)
|
||||
return;
|
||||
|
||||
TimeSpan now = Clock.UnixTime;
|
||||
|
||||
using var eventsHandle = ListPool<ServiceEvent>.Instance.Get(out List<ServiceEvent> events);
|
||||
events.AddRange(_events.Values);
|
||||
|
||||
foreach (ServiceEvent serviceEvent in events)
|
||||
{
|
||||
if (now < serviceEvent.FireTime)
|
||||
continue;
|
||||
|
||||
// Important: remove before triggering, because triggering can schedule another event with the same handle.
|
||||
_events.Remove(serviceEvent.Handle);
|
||||
serviceEvent.Trigger();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ScheduleEvent(THandle handle, TimeSpan delay, Action<TEventData> callback, TEventData eventData = default)
|
||||
{
|
||||
if (callback == null) return Logger.WarnReturn(false, "ScheduleEvent(): callback == null");
|
||||
|
||||
CancelEvent(handle);
|
||||
|
||||
TimeSpan fireTime = Clock.UnixTime + delay;
|
||||
ServiceEvent @event = new(handle, fireTime, callback, eventData);
|
||||
_events.Add(handle, @event);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CancelEvent(THandle handle)
|
||||
{
|
||||
return _events.Remove(handle);
|
||||
}
|
||||
|
||||
private readonly struct ServiceEvent(THandle handle, TimeSpan fireTime, Action<TEventData> callback, TEventData eventData)
|
||||
{
|
||||
public readonly THandle Handle = handle;
|
||||
public readonly TimeSpan FireTime = fireTime;
|
||||
public readonly Action<TEventData> Callback = callback;
|
||||
public readonly TEventData EventData = eventData;
|
||||
|
||||
public void Trigger()
|
||||
{
|
||||
Callback(EventData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/MHServerEmu.Core/System/TokenManager.cs
Normal file
93
src/MHServerEmu.Core/System/TokenManager.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using System.Security.Cryptography;
|
||||
|
||||
namespace MHServerEmu.Core.System
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates random cryptographic tokens bound to <typeparamref name="T"/> values for accessing restricted APIs. This class is thread-safe.
|
||||
/// </summary>
|
||||
public class TokenManager<T>
|
||||
{
|
||||
private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
|
||||
private readonly Dictionary<string, T> _lookup = new();
|
||||
|
||||
private readonly byte[] _buffer;
|
||||
|
||||
public int TokenSize { get => _buffer.Length; }
|
||||
public int Count { get => _lookup.Count; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="TokenManager{T}"/> with the specified token size in bytes.
|
||||
/// </summary>
|
||||
public TokenManager(int tokenSize = 16)
|
||||
{
|
||||
_buffer = new byte[tokenSize];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new token for the provided <typeparamref name="T"/> value.
|
||||
/// </summary>
|
||||
public string GenerateToken(T lookupValue)
|
||||
{
|
||||
lock (_lookup)
|
||||
{
|
||||
string token;
|
||||
|
||||
// The probability of generating the same token should be low, but it's still possible in theory.
|
||||
do
|
||||
{
|
||||
_rng.GetBytes(_buffer);
|
||||
token = Convert.ToHexString(_buffer);
|
||||
}
|
||||
while (_lookup.TryAdd(token, lookupValue) == false);
|
||||
|
||||
Array.Clear(_buffer);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the provided token. Returns <see langword="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool RemoveToken(string token)
|
||||
{
|
||||
lock (_lookup)
|
||||
return _lookup.Remove(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all previously added tokens.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lookup)
|
||||
_lookup.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an existing token to this manager. Returns <see cref="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool ImportToken(string token, T value)
|
||||
{
|
||||
lock (_lookup)
|
||||
return _lookup.TryAdd(token, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies tokens to the provided <see cref="List{T}"/>.
|
||||
/// </summary>
|
||||
public void ExportTokens(List<KeyValuePair<string, T>> tokens)
|
||||
{
|
||||
lock (_lookup)
|
||||
tokens.AddRange(_lookup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <typeparamref name="T"/> value associated with the provided token. Returns <see langword="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool TryGetValue(string token, out T value)
|
||||
{
|
||||
lock (_lookup)
|
||||
return _lookup.TryGetValue(token, out value);
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/MHServerEmu.Core/Threading/TaskManager.cs
Normal file
98
src/MHServerEmu.Core/Threading/TaskManager.cs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
namespace MHServerEmu.Core.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages awaitable custom <see cref="Task{TResult}"/> instances that can be manually completed.
|
||||
/// </summary>
|
||||
public class TaskManager<T>
|
||||
{
|
||||
private readonly Dictionary<ulong, TaskCompletionSource<T>> _pendingTasks = new();
|
||||
private ulong _currentTaskId = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="Task{TResult}"/>.
|
||||
/// </summary>
|
||||
public Handle CreateTask()
|
||||
{
|
||||
lock (_pendingTasks)
|
||||
{
|
||||
ulong taskId = _currentTaskId++;
|
||||
TaskCompletionSource<T> tcs = new();
|
||||
_pendingTasks.Add(taskId, tcs);
|
||||
return new(taskId, tcs.Task, this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the <see cref="Task{TResult}"/> with the specified id using the provided result data. Returns <see langword="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool CompleteTask(ulong taskId, T result)
|
||||
{
|
||||
TaskCompletionSource<T> tcs = null;
|
||||
|
||||
lock (_pendingTasks)
|
||||
{
|
||||
if (_pendingTasks.Remove(taskId, out tcs) == false)
|
||||
return false;
|
||||
}
|
||||
|
||||
tcs.SetResult(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the <see cref="Task{TResult}"/> with the specified id. Returns <see langword="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool CancelTask(ulong taskId)
|
||||
{
|
||||
TaskCompletionSource<T> tcs = null;
|
||||
|
||||
lock (_pendingTasks)
|
||||
{
|
||||
if (_pendingTasks.Remove(taskId, out tcs) == false)
|
||||
return false;
|
||||
}
|
||||
|
||||
tcs.SetCanceled();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels all current tasks.
|
||||
/// </summary>
|
||||
public void CancelAllTasks()
|
||||
{
|
||||
lock (_pendingTasks)
|
||||
{
|
||||
foreach (var kvp in _pendingTasks)
|
||||
kvp.Value.SetCanceled();
|
||||
|
||||
_pendingTasks.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a <see cref="Task{TResult}"/> managed by a <see cref="TaskManager{T}"/>.
|
||||
/// </summary>
|
||||
public readonly struct Handle
|
||||
{
|
||||
public readonly ulong Id;
|
||||
public readonly Task<T> Task;
|
||||
public readonly TaskManager<T> Manager;
|
||||
|
||||
internal Handle(ulong id, Task<T> task, TaskManager<T> manager)
|
||||
{
|
||||
Id = id;
|
||||
Task = task;
|
||||
Manager = manager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the <see cref="Task{TResult}"/> represented by this handle.
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
Manager.CancelTask(Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ namespace MHServerEmu.Core.VectorMath
|
|||
return Yaw == other.Yaw && Pitch == other.Pitch && Roll == other.Roll;
|
||||
}
|
||||
|
||||
public static bool IsFinite(Orientation v)
|
||||
public static bool IsFinite(ref Orientation v)
|
||||
{
|
||||
return float.IsFinite(v.Yaw) && float.IsFinite(v.Pitch) && float.IsFinite(v.Roll);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,8 @@ namespace MHServerEmu.Core.VectorMath
|
|||
|
||||
public static Aabb2 operator *(in Transform3 t, in Aabb2 b)
|
||||
{
|
||||
var points = b.GetPoints();
|
||||
Span<Point2> points = stackalloc Point2[4];
|
||||
b.GetPoints(points);
|
||||
var box = new Aabb2();
|
||||
foreach (Point2 point in points)
|
||||
box.Expand(t * new Point2(point.X, point.Y));
|
||||
|
|
|
|||
|
|
@ -38,13 +38,18 @@ namespace MHServerEmu.DatabaseAccess
|
|||
/// <summary>
|
||||
/// Queries the name of the player with the specified id. Returns <see langword="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool TryGetPlayerName(ulong id, out string playerName);
|
||||
public bool TryGetPlayerName(ulong playerDbId, out string playerName);
|
||||
|
||||
/// <summary>
|
||||
/// Queries the names of all registered players from the database and adds them to the provided <see cref="Dictionary{TKey, TValue}"/>.
|
||||
/// </summary>
|
||||
public bool GetPlayerNames(Dictionary<ulong, string> playerNames);
|
||||
|
||||
/// <summary>
|
||||
/// Queries last logout time for the player with the specified id. Returns <see langword="true"/> if successful.
|
||||
/// </summary>
|
||||
public bool TryGetLastLogoutTime(ulong playerDbId, out long lastLogoutTime);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a new <see cref="DBAccount"/> with all of its data into the database.
|
||||
/// </summary>
|
||||
|
|
@ -64,5 +69,30 @@ namespace MHServerEmu.DatabaseAccess
|
|||
/// Saves persistent game data stored in the database for the provided <see cref="DBAccount"/>.
|
||||
/// </summary>
|
||||
public bool SavePlayerData(DBAccount account);
|
||||
|
||||
/// <summary>
|
||||
/// Loads <see cref="DBGuild"/> instances stored in the database and adds them to the provided <see cref="List{T}"/>.
|
||||
/// </summary>
|
||||
public bool LoadGuilds(List<DBGuild> guilds);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts or updates an existing <see cref="DBGuild"/> instance stored in the database.
|
||||
/// </summary>
|
||||
public bool SaveGuild(DBGuild guild);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a stored <see cref="DBGuild"/> instance with all of its members from the database.
|
||||
/// </summary>
|
||||
public bool DeleteGuild(DBGuild guild);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts or updates an existing <see cref="DBGuildMember"/> instance stored in the database.
|
||||
/// </summary>
|
||||
public bool SaveGuildMember(DBGuildMember guildMember);
|
||||
|
||||
/// <summary>
|
||||
/// Delets a store <see cref="DBGuildMember"/> instance from the database.
|
||||
/// </summary>
|
||||
public bool DeleteGuildMember(DBGuildMember guildMember);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
using System.Text.Json;
|
||||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.RateLimiting;
|
||||
using MHServerEmu.DatabaseAccess.Models;
|
||||
|
||||
namespace MHServerEmu.DatabaseAccess.Json
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializes <see cref="DBAccount"/> instances to JSON.
|
||||
/// </summary>
|
||||
public class DBAccountJsonSerializer
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
private readonly JsonSerializerOptions _options = new();
|
||||
private readonly TimeLeakyBucketCollection<ulong> _rateLimiter = new(TimeSpan.FromMinutes(30), 5);
|
||||
|
||||
public static DBAccountJsonSerializer Instance { get; } = new();
|
||||
|
||||
private DBAccountJsonSerializer()
|
||||
{
|
||||
_options.Converters.Add(new DBEntityCollectionJsonConverter());
|
||||
}
|
||||
|
||||
public bool TrySerializeAccount(DBAccount account, bool checkRateLimit, out string json)
|
||||
{
|
||||
json = string.Empty;
|
||||
|
||||
if (account == null) return Logger.WarnReturn(false, "TrySerializeAccount(): account == null");
|
||||
|
||||
if (checkRateLimit && _rateLimiter.AddTime((ulong)account.Id) == false)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
json = JsonSerializer.Serialize(account, _options);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error($"Failed to serialize account {account}: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,7 @@ namespace MHServerEmu.DatabaseAccess.Json
|
|||
public bool TryQueryAccountByEmail(string email, out DBAccount account)
|
||||
{
|
||||
account = _account;
|
||||
account.MigrationData.Reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +89,12 @@ namespace MHServerEmu.DatabaseAccess.Json
|
|||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetLastLogoutTime(ulong playerDbId, out long lastLogoutTime)
|
||||
{
|
||||
lastLogoutTime = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool InsertAccount(DBAccount account)
|
||||
{
|
||||
return Logger.WarnReturn(false, "InsertAccount(): Operation not supported");
|
||||
|
|
@ -117,6 +124,37 @@ namespace MHServerEmu.DatabaseAccess.Json
|
|||
return true;
|
||||
}
|
||||
|
||||
#region Guilds
|
||||
|
||||
// TODO: Guilds are currently not supported by the JSON backend.
|
||||
|
||||
public bool LoadGuilds(List<DBGuild> guilds)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SaveGuild(DBGuild guild)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DeleteGuild(DBGuild guild)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SaveGuildMember(DBGuildMember guildMember)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DeleteGuildMember(DBGuildMember guildMember)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates a backup of the account file if enough time has passed since the last one.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyVersion>0.7.0.0</AssemblyVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
|
@ -20,6 +20,8 @@
|
|||
<None Remove="SQLite\Scripts\Migrations\1.sql" />
|
||||
<None Remove="SQLite\Scripts\Migrations\2.sql" />
|
||||
<None Remove="SQLite\Scripts\Migrations\3.sql" />
|
||||
<None Remove="SQLite\Scripts\Migrations\4.sql" />
|
||||
<None Remove="SQLite\Scripts\Migrations\5.sql" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -29,6 +31,8 @@
|
|||
<EmbeddedResource Include="SQLite\Scripts\Migrations\1.sql" />
|
||||
<EmbeddedResource Include="SQLite\Scripts\Migrations\2.sql" />
|
||||
<EmbeddedResource Include="SQLite\Scripts\Migrations\3.sql" />
|
||||
<EmbeddedResource Include="SQLite\Scripts\Migrations\4.sql" />
|
||||
<EmbeddedResource Include="SQLite\Scripts\Migrations\5.sql" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ namespace MHServerEmu.DatabaseAccess.Models
|
|||
[Flags]
|
||||
public enum AccountFlags
|
||||
{
|
||||
None = 0,
|
||||
IsBanned = 1 << 0,
|
||||
IsArchived = 1 << 1,
|
||||
IsPasswordExpired = 1 << 2,
|
||||
DEPRECATEDLinuxCompatibilityMode = 1 << 3, // This flag used to disable session token verification, but it is no longer needed for Linux users
|
||||
IsWhitelisted = 1 << 4,
|
||||
None = 0,
|
||||
IsBanned = 1 << 0,
|
||||
IsArchived = 1 << 1,
|
||||
IsPasswordExpired = 1 << 2,
|
||||
// 1 << 3 was previously used in 0.x for the Linux compatibility mode, it should not be set in any 1.x+ databases.
|
||||
IsWhitelisted = 1 << 4,
|
||||
BypassLoginQueue = 1 << 5,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -29,8 +30,12 @@ namespace MHServerEmu.DatabaseAccess.Models
|
|||
/// </summary>
|
||||
public class DBAccount
|
||||
{
|
||||
private const int LockTimeoutMS = 3000;
|
||||
|
||||
private static readonly IdGenerator IdGenerator = new(IdType.Player, 0);
|
||||
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
public long Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string PlayerName { get; set; }
|
||||
|
|
@ -47,6 +52,10 @@ namespace MHServerEmu.DatabaseAccess.Models
|
|||
public DBEntityCollection Items { get; init; } = new();
|
||||
public DBEntityCollection ControlledEntities { get; init; } = new();
|
||||
|
||||
// Imitate ReplicateForTransfer behavior by having a DBEntityCollection that doesn't get saved to the database.
|
||||
[JsonIgnore]
|
||||
public DBEntityCollection TransferredEntities { get; } = new();
|
||||
|
||||
// MigrationData is explicitly not saved and exists only as long as the current session does
|
||||
[JsonIgnore]
|
||||
public MigrationData MigrationData { get; } = new();
|
||||
|
|
@ -88,12 +97,66 @@ namespace MHServerEmu.DatabaseAccess.Models
|
|||
return $"{PlayerName} (0x{Id:X})";
|
||||
}
|
||||
|
||||
public LockScope Lock()
|
||||
{
|
||||
bool lockTaken = _semaphore.Wait(LockTimeoutMS);
|
||||
return new(this, lockTaken);
|
||||
}
|
||||
|
||||
public void Unlock()
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
public void ClearEntities()
|
||||
{
|
||||
Avatars.Clear();
|
||||
TeamUps.Clear();
|
||||
Items.Clear();
|
||||
ControlledEntities.Clear();
|
||||
TransferredEntities.Clear();
|
||||
}
|
||||
|
||||
public EntityUpdateScope BeginEntityUpdate()
|
||||
{
|
||||
Avatars.BeginUpdate();
|
||||
TeamUps.BeginUpdate();
|
||||
Items.BeginUpdate();
|
||||
ControlledEntities.BeginUpdate();
|
||||
TransferredEntities.BeginUpdate();
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
public void EndEntityUpdate()
|
||||
{
|
||||
Avatars.EndUpdate();
|
||||
TeamUps.EndUpdate();
|
||||
Items.EndUpdate();
|
||||
ControlledEntities.EndUpdate();
|
||||
TransferredEntities.EndUpdate();
|
||||
}
|
||||
|
||||
public readonly struct LockScope(DBAccount account, bool lockTaken) : IDisposable
|
||||
{
|
||||
public readonly DBAccount Account = account;
|
||||
public readonly bool LockTaken = lockTaken;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (LockTaken)
|
||||
Account.Unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct EntityUpdateScope(DBAccount account) : IDisposable
|
||||
{
|
||||
public readonly DBAccount Account = account;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Account.EndEntityUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,15 @@ namespace MHServerEmu.DatabaseAccess.Models
|
|||
/// </summary>
|
||||
public class DBEntityCollection
|
||||
{
|
||||
// TODO: Calculate checksum for added entities and update only those that changed
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
private readonly Dictionary<long, DBEntity> _allEntities = new(); // All DBEntity instances stored in this collection
|
||||
private readonly Dictionary<long, List<DBEntity>> _bucketedEntities = new(); // Stored DBEntity bucketed per container
|
||||
|
||||
private Dictionary<long, DBEntity> _allEntities = new(); // All DBEntity instances stored in this collection
|
||||
|
||||
private Dictionary<long, DBEntity> _dirtyEntities = new();
|
||||
private bool _isUpdating;
|
||||
|
||||
public IEnumerable<DBEntity> Entries { get => _allEntities.Values; }
|
||||
public int Count { get => _allEntities.Count; }
|
||||
|
||||
|
|
@ -77,6 +80,59 @@ namespace MHServerEmu.DatabaseAccess.Models
|
|||
bucket.Clear();
|
||||
}
|
||||
|
||||
public void BeginUpdate()
|
||||
{
|
||||
if (_isUpdating)
|
||||
throw new InvalidOperationException("Entity update is already in progress.");
|
||||
|
||||
// Do not remove entities yet, we will reuse them if they are added back.
|
||||
(_allEntities, _dirtyEntities) = (_dirtyEntities, _allEntities);
|
||||
|
||||
foreach (List<DBEntity> bucket in _bucketedEntities.Values)
|
||||
bucket.Clear();
|
||||
|
||||
_isUpdating = true;
|
||||
}
|
||||
|
||||
public void EndUpdate()
|
||||
{
|
||||
if (_isUpdating == false)
|
||||
throw new InvalidOperationException("Entity update is not in progress.");
|
||||
|
||||
_dirtyEntities.Clear();
|
||||
|
||||
_isUpdating = false;
|
||||
}
|
||||
|
||||
public bool UpdateEntity(long dbGuid, long containerDbGuid, long inventoryProtoGuid, uint slot, long entityProtoGuid, Span<byte> archiveData)
|
||||
{
|
||||
if (_isUpdating == false)
|
||||
throw new InvalidOperationException("Entity update is not in progress.");
|
||||
|
||||
// Reuse existing DBEntity instances if possible.
|
||||
if (_dirtyEntities.Remove(dbGuid, out DBEntity dbEntity) == false)
|
||||
dbEntity = new();
|
||||
|
||||
dbEntity.DbGuid = dbGuid;
|
||||
dbEntity.ContainerDbGuid = containerDbGuid;
|
||||
dbEntity.InventoryProtoGuid = inventoryProtoGuid;
|
||||
dbEntity.Slot = slot;
|
||||
dbEntity.EntityProtoGuid = entityProtoGuid;
|
||||
|
||||
// Do not allocate new archive data buffers if we can reuse the ones we already have.
|
||||
Span<byte> oldArchiveData = dbEntity.ArchiveData ?? Span<byte>.Empty;
|
||||
if (archiveData.SequenceEqual(oldArchiveData) == false)
|
||||
{
|
||||
// Overwrite existing buffer if the size matches.
|
||||
if (archiveData.Length == oldArchiveData.Length)
|
||||
archiveData.CopyTo(oldArchiveData);
|
||||
else
|
||||
dbEntity.ArchiveData = archiveData.ToArray();
|
||||
}
|
||||
|
||||
return Add(dbEntity);
|
||||
}
|
||||
|
||||
public bool Contains(long dbGuid)
|
||||
{
|
||||
return _allEntities.ContainsKey(dbGuid);
|
||||
|
|
|
|||
29
src/MHServerEmu.DatabaseAccess/Models/DBGuild.cs
Normal file
29
src/MHServerEmu.DatabaseAccess/Models/DBGuild.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
namespace MHServerEmu.DatabaseAccess.Models
|
||||
{
|
||||
public class DBGuild
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Motd { get; set; }
|
||||
public long CreatorDbGuid { get; set; }
|
||||
public long CreationTime { get; set; }
|
||||
|
||||
// CreatorDbGuid and CreationTime are just additional metadata for tracking/moderation.
|
||||
|
||||
public List<DBGuildMember> Members { get; init; } = new();
|
||||
|
||||
public DBGuild(long id, string name, string motd, long creatorDbGuid, long creationTime)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Motd = motd;
|
||||
CreatorDbGuid = creatorDbGuid;
|
||||
CreationTime = creationTime;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} ({Id})";
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/MHServerEmu.DatabaseAccess/Models/DBGuildMember.cs
Normal file
21
src/MHServerEmu.DatabaseAccess/Models/DBGuildMember.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
namespace MHServerEmu.DatabaseAccess.Models
|
||||
{
|
||||
public class DBGuildMember
|
||||
{
|
||||
public long PlayerDbGuid { get; set; }
|
||||
public long GuildId { get; set; }
|
||||
public long Membership { get; set; } // This needs to be long for our Dapper/System.Data.SQLite combo.
|
||||
|
||||
public DBGuildMember(long playerDbGuid, long guildId, long membership)
|
||||
{
|
||||
PlayerDbGuid = playerDbGuid;
|
||||
GuildId = guildId;
|
||||
Membership = membership;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"guildId={GuildId}, playerDbGuid=0x{PlayerDbGuid:X}, membership={Membership}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,26 +4,48 @@ namespace MHServerEmu.DatabaseAccess.Models
|
|||
{
|
||||
public class MigrationData
|
||||
{
|
||||
// Store everything here as ulong, PropertyCollection will sort it out game-side
|
||||
private readonly Dictionary<ulong, List<(ulong, ulong)>> _properties = new(32);
|
||||
|
||||
public bool IsInErrorState { get; set; }
|
||||
public bool SkipNextUpdate { get; set; }
|
||||
|
||||
public bool IsFirstLoad { get; set; } = true;
|
||||
|
||||
// Store everything here as ulong, PropertyCollection will sort it out game-side
|
||||
public List<KeyValuePair<ulong, ulong>> PlayerProperties { get; } = new(256);
|
||||
public List<(ulong, ulong)> WorldView { get; } = new();
|
||||
public byte[] MatchQueueStatus { get; set; }
|
||||
public List<CommunityMemberBroadcast> CommunityStatus { get; } = new();
|
||||
|
||||
// TODO: Summoned inventory
|
||||
|
||||
public MigrationData() { }
|
||||
|
||||
public List<(ulong, ulong)> GetOrCreatePropertyList(ulong entityDbId)
|
||||
{
|
||||
if (_properties.TryGetValue(entityDbId, out List<(ulong, ulong)> list) == false)
|
||||
{
|
||||
list = new();
|
||||
_properties.Add(entityDbId, list);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public void RemovePropertyList(ulong entityDbId)
|
||||
{
|
||||
_properties.Remove(entityDbId);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
IsInErrorState = false;
|
||||
SkipNextUpdate = false;
|
||||
|
||||
IsFirstLoad = true;
|
||||
PlayerProperties.Clear();
|
||||
|
||||
// Properties for summoned entities need to be migrated, and these have arbitrary runtime dbIds, so just clear everything.
|
||||
_properties.Clear();
|
||||
|
||||
WorldView.Clear();
|
||||
MatchQueueStatus = null;
|
||||
CommunityStatus.Clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
/// </summary>
|
||||
public class SQLiteDBManager : IDBManager
|
||||
{
|
||||
private const int CurrentSchemaVersion = 4; // Increment this when making changes to the database schema
|
||||
private const int CurrentSchemaVersion = 6; // Increment this when making changes to the database schema
|
||||
private const int MinimumSchemaVersion = 6; // Used to ignore legacy 0.x database files.
|
||||
private const int NumTestAccounts = 5; // Number of test accounts to create for new database files
|
||||
private const int NumPlayerDataWriteAttempts = 3; // Number of write attempts to do when saving player data
|
||||
|
||||
|
|
@ -37,7 +38,10 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
var config = ConfigManager.Instance.GetConfig<SQLiteDBManagerConfig>();
|
||||
|
||||
_dbFilePath = Path.Combine(FileHelper.DataDirectory, config.FileName);
|
||||
_connectionString = $"Data Source={_dbFilePath};Synchronous=NORMAL;";
|
||||
_connectionString = $"Data Source={_dbFilePath};Synchronous=NORMAL;foreign_keys=OFF;";
|
||||
|
||||
// TODO: Foreign key constraints are explicitly disabled for now because our Item table references
|
||||
// multiple parent tables (Player / Avatar / TeamUp) at the same time. Need to find an elegant way to fix that.
|
||||
|
||||
if (File.Exists(_dbFilePath) == false)
|
||||
{
|
||||
|
|
@ -62,10 +66,10 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
public bool TryQueryAccountByEmail(string email, out DBAccount account)
|
||||
{
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
var accounts = connection.Query<DBAccount>("SELECT * FROM Account WHERE Email = @Email", new { Email = email });
|
||||
|
||||
// Associated player data is loaded separately
|
||||
account = accounts.FirstOrDefault();
|
||||
// This is just the base account entry, associated player data is loaded separately
|
||||
account = connection.QueryFirstOrDefault<DBAccount>("SELECT * FROM Account WHERE Email = @Email COLLATE NOCASE", new { Email = email });
|
||||
|
||||
return account != null;
|
||||
}
|
||||
|
||||
|
|
@ -90,11 +94,11 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetPlayerName(ulong id, out string playerName)
|
||||
public bool TryGetPlayerName(ulong playerDbId, out string playerName)
|
||||
{
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
playerName = connection.QueryFirstOrDefault<string>("SELECT PlayerName FROM Account WHERE Id = @Id", new { Id = (long)id });
|
||||
playerName = connection.QueryFirstOrDefault<string>("SELECT PlayerName FROM Account WHERE Id = @Id", new { Id = (long)playerDbId });
|
||||
|
||||
return string.IsNullOrWhiteSpace(playerName) == false;
|
||||
}
|
||||
|
|
@ -111,6 +115,15 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
return playerNames.Count > 0;
|
||||
}
|
||||
|
||||
public bool TryGetLastLogoutTime(ulong playerDbId, out long lastLogoutTime)
|
||||
{
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
lastLogoutTime = connection.QueryFirstOrDefault<long>("SELECT LastLogoutTime FROM Player WHERE DbGuid = @DbGuid", new { DbGuid = (long)playerDbId });
|
||||
|
||||
return lastLogoutTime > 0;
|
||||
}
|
||||
|
||||
public bool InsertAccount(DBAccount account)
|
||||
{
|
||||
lock (_writeLock)
|
||||
|
|
@ -204,6 +217,129 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
return Logger.WarnReturn(false, $"SavePlayerData(): Failed to write player data for account [{account}]");
|
||||
}
|
||||
|
||||
public bool LoadGuilds(List<DBGuild> outGuilds)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
IEnumerable<DBGuild> guildQueryResult = connection.Query<DBGuild>("SELECT * FROM Guild");
|
||||
IEnumerable<DBGuildMember> memberQueryResult = connection.Query<DBGuildMember>("SELECT * FROM GuildMember");
|
||||
|
||||
outGuilds.AddRange(guildQueryResult);
|
||||
|
||||
// This is going to be called only on server startup, so it's fine not to pool this.
|
||||
Dictionary<long, DBGuild> guildLookup = new(outGuilds.Count);
|
||||
foreach (DBGuild guild in outGuilds)
|
||||
guildLookup.Add(guild.Id, guild);
|
||||
|
||||
foreach (DBGuildMember member in memberQueryResult)
|
||||
{
|
||||
if (guildLookup.TryGetValue(member.GuildId, out DBGuild guild) == false)
|
||||
{
|
||||
Logger.Warn($"LoadGuilds(): Found orphan member [{member}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
guild.Members.Add(member);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
outGuilds.Clear();
|
||||
Logger.ErrorException(e, nameof(LoadGuilds));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SaveGuild(DBGuild guild)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
int inserted = connection.Execute("INSERT OR IGNORE INTO Guild (Id, Name, Motd, CreatorDbGuid, CreationTime) VALUES (@Id, @Name, @Motd, @CreatorDbGuid, @CreationTime)", guild);
|
||||
|
||||
// Only name and MOTD should be mutable after creation.
|
||||
if (inserted == 0)
|
||||
connection.Execute("UPDATE Guild SET Name=@Name, Motd=@Motd WHERE Id=@Id", guild);
|
||||
|
||||
Logger.Trace($"SaveGuild(): {guild}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorException(e, nameof(SaveGuild));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool DeleteGuild(DBGuild guild)
|
||||
{
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
using SQLiteTransaction transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Enable foreign key constraints in the connection string and just delete the row from the parent table when we fix the Item table.
|
||||
connection.Execute("DELETE FROM GuildMember WHERE GuildId = @Id", guild, transaction);
|
||||
connection.Execute("DELETE FROM Guild WHERE Id = @Id", guild, transaction);
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
Logger.Trace($"DeleteGuild(): {guild}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
transaction.Rollback();
|
||||
Logger.ErrorException(e, nameof(DeleteGuild));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SaveGuildMember(DBGuildMember guildMember)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
int inserted = connection.Execute("INSERT OR IGNORE INTO GuildMember (PlayerDbGuid, GuildId, Membership) VALUES (@PlayerDbGuid, @GuildId, @Membership)", guildMember);
|
||||
|
||||
// Only membership should be mutable after creation.
|
||||
if (inserted == 0)
|
||||
connection.Execute("UPDATE GuildMember SET Membership=@Membership WHERE PlayerDbGuid=@PlayerDbGuid", guildMember);
|
||||
|
||||
Logger.Trace($"SaveGuildMember(): {guildMember}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorException(e, nameof(SaveGuildMember));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool DeleteGuildMember(DBGuildMember guildMember)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
connection.Execute("DELETE FROM GuildMember WHERE PlayerDbGuid = @PlayerDbGuid", guildMember);
|
||||
|
||||
Logger.Trace($"DeleteGuildMember(): {guildMember}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorException(e, nameof(DeleteGuildMember));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and opens a new <see cref="SQLiteConnection"/>.
|
||||
/// </summary>
|
||||
|
|
@ -262,6 +398,18 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
if (schemaVersion > CurrentSchemaVersion)
|
||||
return Logger.ErrorReturn(false, $"Initialize(): Existing database file uses unsupported schema version {schemaVersion} (current = {CurrentSchemaVersion})");
|
||||
|
||||
if (schemaVersion < MinimumSchemaVersion)
|
||||
{
|
||||
Logger.Warn($"Found existing database file with legacy schema version {schemaVersion}, which is not supported by this version of MHServerEmu");
|
||||
|
||||
// Need to dispose the connection before moving the database file so that the file is not in use.
|
||||
connection.Dispose();
|
||||
File.Move(_dbFilePath, $"{_dbFilePath}.old");
|
||||
|
||||
InitializeDatabaseFile();
|
||||
return true;
|
||||
}
|
||||
|
||||
Logger.Info($"Found existing database file with schema version {schemaVersion} (current = {CurrentSchemaVersion})");
|
||||
|
||||
if (schemaVersion == CurrentSchemaVersion)
|
||||
|
|
@ -323,9 +471,9 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
// Update player entity
|
||||
if (account.Player != null)
|
||||
{
|
||||
connection.Execute(@$"INSERT OR IGNORE INTO Player (DbGuid) VALUES (@DbGuid)", account.Player, transaction);
|
||||
connection.Execute(@$"UPDATE Player SET ArchiveData=@ArchiveData, StartTarget=@StartTarget,
|
||||
AOIVolume=@AOIVolume, GazillioniteBalance=@GazillioniteBalance WHERE DbGuid = @DbGuid",
|
||||
connection.Execute(@"INSERT OR IGNORE INTO Player (DbGuid) VALUES (@DbGuid)", account.Player, transaction);
|
||||
connection.Execute(@"UPDATE Player SET ArchiveData=@ArchiveData, StartTarget=@StartTarget, AOIVolume=@AOIVolume,
|
||||
GazillioniteBalance=@GazillioniteBalance, LastLogoutTime=@LastLogoutTime WHERE DbGuid = @DbGuid",
|
||||
account.Player, transaction);
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -67,24 +67,19 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
public void UpdateEntities(SQLiteConnection connection, SQLiteTransaction transaction, long containerDbGuid, DBEntityCollection dbEntityCollection)
|
||||
{
|
||||
// Delete items that no longer belong to this account
|
||||
List<long> entitiesToDelete = ListPool<long>.Instance.Get();
|
||||
using var entitiesToDeleteHandle = ListPool<long>.Instance.Get(out List<long> entitiesToDelete);
|
||||
GetEntitiesToDelete(connection, containerDbGuid, dbEntityCollection, entitiesToDelete);
|
||||
|
||||
try
|
||||
{
|
||||
if (entitiesToDelete.Count > 0)
|
||||
connection.Execute(_deleteQuery, new { EntitiesToDelete = entitiesToDelete });
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Make sure the list is returned to the pool even if the deletion query fails.
|
||||
ListPool<long>.Instance.Return(entitiesToDelete);
|
||||
}
|
||||
if (entitiesToDelete.Count > 0)
|
||||
connection.Execute(_deleteQuery, new { EntitiesToDelete = entitiesToDelete });
|
||||
|
||||
// Insert and update
|
||||
IReadOnlyList<DBEntity> entries = dbEntityCollection.GetEntriesForContainer(containerDbGuid);
|
||||
connection.Execute(_insertQuery, entries, transaction);
|
||||
connection.Execute(_updateQuery, entries, transaction);
|
||||
if (entries.Count > 0)
|
||||
{
|
||||
connection.Execute(_insertQuery, entries, transaction);
|
||||
connection.Execute(_updateQuery, entries, transaction);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
-- Initialize a new database file using the current schema version
|
||||
|
||||
PRAGMA user_version=4;
|
||||
PRAGMA user_version=6;
|
||||
PRAGMA journal_mode=WAL;
|
||||
|
||||
CREATE TABLE "Account" (
|
||||
|
|
@ -20,6 +20,7 @@ CREATE TABLE "Player" (
|
|||
"StartTarget" INTEGER,
|
||||
"AOIVolume" INTEGER,
|
||||
"GazillioniteBalance" INTEGER,
|
||||
"LastLogoutTime" INTEGER,
|
||||
FOREIGN KEY("DbGuid") REFERENCES "Account"("Id") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
|
@ -70,6 +71,24 @@ CREATE TABLE "ControlledEntity" (
|
|||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
||||
CREATE TABLE "Guild" (
|
||||
"Id" INTEGER NOT NULL UNIQUE,
|
||||
"Name" TEXT NOT NULL UNIQUE,
|
||||
"Motd" TEXT NOT NULL,
|
||||
"CreatorDbGuid" INTEGER,
|
||||
"CreationTime" INTEGER,
|
||||
PRIMARY KEY("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE "GuildMember" (
|
||||
"PlayerDbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"Membership" INTEGER NOT NULL,
|
||||
FOREIGN KEY("PlayerDbGuid") REFERENCES "Account"("Id") ON DELETE CASCADE,
|
||||
FOREIGN KEY("GuildId") REFERENCES "Guild"("Id") ON DELETE CASCADE,
|
||||
PRIMARY KEY("PlayerDbGuid")
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_Avatar_ContainerDbGuid" ON "Avatar" ("ContainerDbGuid");
|
||||
CREATE INDEX "IX_TeamUp_ContainerDbGuid" ON "TeamUp" ("ContainerDbGuid");
|
||||
CREATE INDEX "IX_Item_ContainerDbGuid" ON "Item" ("ContainerDbGuid");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
-- Add guild data.
|
||||
|
||||
-- Add last logout time to players and initialize it to 0.
|
||||
ALTER TABLE Player ADD COLUMN LastLogoutTime INTEGER;
|
||||
UPDATE Player SET LastLogoutTime=0;
|
||||
|
||||
-- Create guild tables.
|
||||
CREATE TABLE "Guild" (
|
||||
"Id" INTEGER NOT NULL UNIQUE,
|
||||
"Name" TEXT NOT NULL UNIQUE,
|
||||
"Motd" TEXT NOT NULL,
|
||||
"CreatorDbGuid" INTEGER,
|
||||
"CreationTime" INTEGER,
|
||||
PRIMARY KEY("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE "GuildMember" (
|
||||
"PlayerDbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"GuildId" INTEGER NOT NULL,
|
||||
"Membership" INTEGER NOT NULL,
|
||||
FOREIGN KEY("PlayerDbGuid") REFERENCES "Account"("Id") ON DELETE CASCADE,
|
||||
FOREIGN KEY("GuildId") REFERENCES "Guild"("Id") ON DELETE CASCADE,
|
||||
PRIMARY KEY("PlayerDbGuid")
|
||||
);
|
||||
|
||||
-- We do not need indexing for these tables because we are going to load all guilds into memory.
|
||||
|
|
@ -0,0 +1 @@
|
|||
-- Empty version, this is used only to prevent 0.x database files from being loaded in 1.x+.
|
||||
|
|
@ -3,7 +3,7 @@ using Google.ProtocolBuffers;
|
|||
using MHServerEmu.Core.Logging;
|
||||
using MHServerEmu.Core.Network;
|
||||
using MHServerEmu.Core.Network.Tcp;
|
||||
using MHServerEmu.Core.System;
|
||||
using MHServerEmu.Core.RateLimiting;
|
||||
using MHServerEmu.DatabaseAccess;
|
||||
using MHServerEmu.DatabaseAccess.Models;
|
||||
|
||||
|
|
@ -14,14 +14,13 @@ namespace MHServerEmu.Frontend
|
|||
/// </summary>
|
||||
public class FrontendClient : TcpClient, IFrontendClient, IDBAccountOwner
|
||||
{
|
||||
// We are currently allowing 50 packets per seconds with up to 10 seconds of burst.
|
||||
// Given our current receive buffer size of 8 KB, this limits client input at about 400 KB/s.
|
||||
private const int RateLimitPacketsPerSecond = 50;
|
||||
private const int RateLimitBurst = RateLimitPacketsPerSecond * 10;
|
||||
// Rate limit at 8 KB/s with a bit of burst allowed.
|
||||
private const int RateLimitBytesPerSecond = 1024 * 8;
|
||||
private const int RateLimitBurst = RateLimitBytesPerSecond * 10;
|
||||
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
private readonly TokenBucket _tokenBucket = new(RateLimitPacketsPerSecond, RateLimitBurst);
|
||||
private readonly TokenBucket _tokenBucket = new(RateLimitBytesPerSecond, RateLimitBurst);
|
||||
private readonly MuxReader _muxReader;
|
||||
|
||||
// We intentionally don't use an array here so that channel state is inlined in FrontendClient
|
||||
|
|
@ -138,7 +137,7 @@ namespace MHServerEmu.Frontend
|
|||
/// </summary>
|
||||
public void HandleIncomingData(byte[] buffer, int length)
|
||||
{
|
||||
if (_tokenBucket.CheckLimit() == false)
|
||||
if (_tokenBucket.CheckLimit(length) == false)
|
||||
{
|
||||
Logger.Error($"HandleIncomingData(): Rate limit exceeded for client [{this}]");
|
||||
Disconnect();
|
||||
|
|
@ -241,10 +240,10 @@ namespace MHServerEmu.Frontend
|
|||
var clientCredentials = messageBuffer.Deserialize<FrontendProtocolMessage>() as ClientCredentials;
|
||||
if (clientCredentials == null) return Logger.ErrorReturn(false, $"OnClientCredentials(): Failed to retrieve message");
|
||||
|
||||
// Routing this message should authenticate the client if the credentials are successfully verified
|
||||
MailboxMessage mailboxMessage = new(messageBuffer.MessageId, clientCredentials);
|
||||
ServiceMessage.RouteMessage routeMessage = new(_client, typeof(FrontendProtocolMessage), mailboxMessage);
|
||||
ServerManager.Instance.SendMessageToService(GameServiceType.PlayerManager, routeMessage);
|
||||
// Routing this message should authenticate the client if the credentials are successfully verified.
|
||||
// If we ever split the frontend into a separate process we will need to replicate session assignment between the processes.
|
||||
ServiceMessage.SessionVerificationRequest sessionVerificationRequest = new(_client, clientCredentials);
|
||||
ServerManager.Instance.SendMessageToService(GameServiceType.PlayerManager, sessionVerificationRequest);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,9 +52,10 @@ namespace MHServerEmu.Frontend
|
|||
}
|
||||
}
|
||||
|
||||
public string GetStatus()
|
||||
public void GetStatus(Dictionary<string, long> statusDict)
|
||||
{
|
||||
return $"Connections: {ConnectionCount} | Clients: {_clients.Count}";
|
||||
statusDict["FrontendConnections"] = ConnectionCount;
|
||||
statusDict["FrontendClients"] = _clients.Count;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyVersion>0.7.0.0</AssemblyVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue