Compare commits
812 Commits
30c89dcd2a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5967414da4 | ||
|
|
da118dc826 | ||
|
|
823eef00ac | ||
|
|
7844c5b76b | ||
|
|
4abd179a23 | ||
|
|
a3e88c7357 | ||
|
|
4e1a9a815f | ||
|
|
de3ae0bca2 | ||
|
|
b1ff4d3a2a | ||
|
|
24b52e14c3 | ||
|
|
2b92baf530 | ||
|
|
819d5f667b | ||
|
|
de6c700bb3 | ||
|
|
3232efd05a | ||
|
|
0c79fee8af | ||
|
|
3d77e146e9 | ||
|
|
a43a25c610 | ||
|
|
3cfde577eb | ||
|
|
85f9c02ced | ||
|
|
9f7fd83626 | ||
|
|
ee8b0a2182 | ||
|
|
6e95e014fa | ||
|
|
61a135b3a7 | ||
|
|
5a81534e84 | ||
|
|
523d835ac0 | ||
|
|
5a7e20efec | ||
|
|
5f47bf0589 | ||
|
|
a58ef20fab | ||
|
|
3999f34f77 | ||
|
|
6f51a2e349 | ||
|
|
de755f8fd0 | ||
|
|
803aa71771 | ||
|
|
4a77066d08 | ||
|
|
c9b5f8569f | ||
|
|
ddbfe91d8b | ||
|
|
74ac6ce940 | ||
|
|
43b0bc2dec | ||
|
|
b953e7831a | ||
|
|
62d93f65e7 | ||
|
|
7dfa9c297e | ||
|
|
f95fd49efd | ||
|
|
ce1a2a3588 | ||
|
|
3739c2a6f9 | ||
|
|
eca7dd86e1 | ||
|
|
e161e3626f | ||
|
|
e1a994ba11 | ||
|
|
82bb99d141 | ||
|
|
f9543a5156 | ||
|
|
174830731c | ||
|
|
3a7f593105 | ||
|
|
f6aa0c3339 | ||
|
|
ecc483a11a | ||
|
|
97c8231b44 | ||
|
|
5f5634d999 | ||
|
|
5bfdb5c32b | ||
|
|
90f1447d48 | ||
|
|
ee3f25438f | ||
|
|
2d8969bed2 | ||
|
|
fa5d50955d | ||
|
|
6574450489 | ||
|
|
0daeb70900 | ||
|
|
061e4f0c51 | ||
|
|
5c76aa7079 | ||
|
|
b327398448 | ||
|
|
d0abb08d5b | ||
|
|
d2cd601802 | ||
|
|
487ee0e726 | ||
|
|
3b35789b47 | ||
|
|
28b6386963 | ||
|
|
1ca0ff344e | ||
|
|
9825944efc | ||
|
|
ca96be3905 | ||
|
|
4b89588c22 | ||
|
|
0051ac0be8 | ||
|
|
918cdeac0e | ||
|
|
13244313f1 | ||
|
|
4ea9864833 | ||
|
|
77057e01b6 | ||
|
|
f030b61645 | ||
|
|
5a44154d30 | ||
|
|
a905954b5c | ||
|
|
99748ba41e | ||
|
|
40ec827342 | ||
|
|
a16a06e389 | ||
|
|
5b37d9493b | ||
|
|
f433a26a6d | ||
|
|
141ba67014 | ||
|
|
d83cf365ac | ||
|
|
24b463f0aa | ||
|
|
c021b40fbe | ||
|
|
36dd93b076 | ||
|
|
3ee1283a2c | ||
|
|
c3da3162ee | ||
|
|
37cd641942 | ||
|
|
87145579e6 | ||
|
|
7ec6381cf1 | ||
|
|
2ee0cbc094 | ||
|
|
6510e4e09b | ||
|
|
34bc35a6b2 | ||
|
|
8352d23164 | ||
|
|
e71971d0b4 | ||
|
|
bceb7965f7 | ||
|
|
c3f052ef30 | ||
|
|
7d054bbe91 | ||
|
|
102d87da3e | ||
|
|
78a68148ce | ||
|
|
f473c54880 | ||
|
|
2eba4b7915 | ||
|
|
39e1d4c42f | ||
|
|
7916f90992 | ||
|
|
8ac2833ce2 | ||
|
|
fbc845526b | ||
|
|
257a979f93 | ||
|
|
ce7be73e49 | ||
|
|
28f2199142 | ||
|
|
80cfa0a07e | ||
|
|
c89632b409 | ||
|
|
5a5a1db2a3 | ||
|
|
0ac84a9509 | ||
|
|
3a9932e307 | ||
|
|
28d92c1e18 | ||
|
|
b62b4af628 | ||
|
|
31d274dd9d | ||
|
|
9c6f3988de | ||
|
|
6439995434 | ||
|
|
603c1b5ad3 | ||
|
|
4552af99c7 | ||
|
|
8e904e9068 | ||
|
|
ca7222a6c7 | ||
|
|
dabf43aefb | ||
|
|
0f862453cb | ||
|
|
d8fdc956ef | ||
|
|
6eb1a589b4 | ||
| c378d3d5f7 | |||
|
|
1a0e0b405a | ||
|
|
7405aac82d | ||
|
|
5204615c28 | ||
|
|
3c160ef695 | ||
|
|
c19ee7de03 | ||
|
|
43881fd988 | ||
|
|
d86b75408b | ||
|
|
218e23ff81 | ||
|
|
f221b299cd | ||
|
|
908f9bee98 | ||
|
|
c1b18601f3 | ||
|
|
8c049bcdcd | ||
|
|
2f220bb863 | ||
|
|
6aa601bb06 | ||
|
|
f810a2ae86 | ||
|
|
3a13bcc99c | ||
|
|
5995f0670c | ||
|
|
1b6586aedc | ||
|
|
977fc78cf6 | ||
|
|
2aea283c6b | ||
|
|
f377068f60 | ||
|
|
81c16590d6 | ||
|
|
1724c06eab | ||
|
|
0a016e9e7f | ||
|
|
39e64c0bf5 | ||
|
|
331337fc51 | ||
|
|
acfdf5679d | ||
|
|
5675fff48c | ||
|
|
8ef327cbe0 | ||
|
|
5b346ef505 | ||
|
|
1a5996d902 | ||
|
|
6d547ff656 | ||
|
|
79a3874d13 | ||
|
|
6ac3c1b9b7 | ||
|
|
4d39164c86 | ||
|
|
a198100547 | ||
|
|
3bf07dd5c5 | ||
|
|
f85228e371 | ||
|
|
ccb4ad8fdf | ||
|
|
97b2588339 | ||
|
|
89a67655fc | ||
|
|
35434fd1d8 | ||
|
|
4cf1bcc07f | ||
|
|
f8c301dc51 | ||
|
|
8805e038b6 | ||
|
|
d6d03a576d | ||
|
|
1a804f5e19 | ||
|
|
07758266d5 | ||
|
|
b4a8048b85 | ||
|
|
8552eb61a8 | ||
|
|
b4463c35e0 | ||
|
|
79c014f9cd | ||
|
|
7e3f31e267 | ||
|
|
799fb24f43 | ||
|
|
e440bd7613 | ||
|
|
10f20f453d | ||
|
|
92d3c755a5 | ||
|
|
b2e6b40e98 | ||
|
|
869698892f | ||
|
|
6e2e7ecf29 | ||
|
|
46f2bdcd9a | ||
|
|
e037539123 | ||
|
|
ec9f592e44 | ||
|
|
23027ccfde | ||
|
|
023128be25 | ||
|
|
578b367a9e | ||
|
|
0dd60f6f6d | ||
|
|
1d470e305a | ||
|
|
7a7fae05ea | ||
|
|
184fe9f59d | ||
|
|
c5caab35ae | ||
|
|
87fdccaddf | ||
|
|
a7171e9ef4 | ||
|
|
0580cb0fef | ||
|
|
66fdc3d189 | ||
|
|
0875180979 | ||
|
|
c40430aaa4 | ||
|
|
7439c45768 | ||
|
|
561ffdd3dc | ||
|
|
215eb19602 | ||
|
|
1a7aa88820 | ||
|
|
442963cb1f | ||
|
|
728b8748a9 | ||
|
|
44591d21b1 | ||
|
|
03f05b4e71 | ||
|
|
e59a8a5094 | ||
|
|
61e41c4de6 | ||
|
|
a52d3df5f9 | ||
|
|
03a28c968b | ||
|
|
856844e845 | ||
|
|
7376e0e58b | ||
|
|
04ddd60d01 | ||
|
|
875ad668aa | ||
|
|
0780eae582 | ||
|
|
8ec1165d9a | ||
|
|
194e21f430 | ||
|
|
cefa4d21ce | ||
|
|
3c32628475 | ||
|
|
5c87d7086c | ||
|
|
2a1fb9268d | ||
|
|
0633f6ec7f | ||
|
|
18bf82cc96 | ||
|
|
7e25858d3c | ||
|
|
61ccbc6c62 | ||
|
|
308a2adbae | ||
|
|
e74c064945 | ||
|
|
9a7bab35e2 | ||
|
|
c25bbfa2df | ||
|
|
00c3576210 | ||
|
|
9cd56bf038 | ||
|
|
6c53e57517 | ||
|
|
85699b63de | ||
|
|
bc04e5760c | ||
|
|
fa31635f02 | ||
|
|
42f6779768 | ||
|
|
5ffe44a981 | ||
|
|
ab7ebc88ba | ||
|
|
6af81c90ca | ||
|
|
076e021752 | ||
|
|
30cf53402c | ||
|
|
f4f2bf8908 | ||
|
|
20d5b138fc | ||
|
|
5adea1c1e5 | ||
|
|
11faff1887 | ||
|
|
bfca38e332 | ||
|
|
00a714e6b8 | ||
|
|
6e0fc50b9f | ||
|
|
334001f844 | ||
|
|
faf9c33500 | ||
|
|
d865b9b202 | ||
|
|
76df53574c | ||
|
|
bd286662fa | ||
|
|
68197fea40 | ||
|
|
d55c96e383 | ||
|
|
06091ff42c | ||
|
|
ce0474258a | ||
|
|
58f0f98262 | ||
|
|
0622f4710b | ||
|
|
6767075dcd | ||
|
|
ab31947c39 | ||
|
|
99af9b6e01 | ||
|
|
40411ba84b | ||
|
|
f6745bd2a6 | ||
|
|
5e8a09c226 | ||
|
|
ed84e4d2df | ||
|
|
a3740d417d | ||
|
|
0b2d127faf | ||
|
|
619e4b50ca | ||
|
|
5b655c8287 | ||
|
|
0d2e307021 | ||
|
|
b60886bae0 | ||
|
|
ec49d28bd4 | ||
|
|
fa0ed36363 | ||
|
|
1f614e4904 | ||
|
|
537dfc1be1 | ||
|
|
12c97dbf90 | ||
|
|
d995a63ff9 | ||
|
|
6702649cac | ||
|
|
d980053959 | ||
|
|
f8b8a87331 | ||
|
|
e0a82d57b4 | ||
|
|
21830ddf6a | ||
|
|
f00e08764c | ||
|
|
b072e61e71 | ||
|
|
e42f5d8e7e | ||
|
|
f1223e471c | ||
|
|
9306f92a35 | ||
|
|
5320dffdd8 | ||
|
|
745454daea | ||
|
|
bcb7e9f49c | ||
|
|
356f7e2e19 | ||
|
|
6f41039c85 | ||
|
|
d0cf39b439 | ||
|
|
d3d0ead712 | ||
|
|
75de7bd557 | ||
|
|
52db89b390 | ||
|
|
b3cc06cd38 | ||
|
|
0aba7e7ccb | ||
|
|
707142bd49 | ||
|
|
133d15e392 | ||
|
|
90d03b3a32 | ||
|
|
8ee19aa66f | ||
|
|
e6d28b017b | ||
|
|
f1835f7aec | ||
|
|
15764ee027 | ||
|
|
3ad96070a3 | ||
|
|
d8366616e0 | ||
|
|
41a1bfb0c2 | ||
|
|
04c9c73ffa | ||
|
|
f9892dda8a | ||
|
|
09d58c1f14 | ||
|
|
705eb31007 | ||
|
|
84024aed83 | ||
|
|
ce3f1fc02e | ||
|
|
5773b8d182 | ||
|
|
e7b64cc669 | ||
|
|
6e268c66f4 | ||
|
|
77c404591a | ||
|
|
65f696dfc3 | ||
|
|
9380bac839 | ||
|
|
6c0c6cafff | ||
|
|
b51d682646 | ||
|
|
4fb5653c28 | ||
|
|
61b0d6093f | ||
|
|
4ba8fe32c4 | ||
|
|
6717ca5236 | ||
|
|
1969c01f3e | ||
|
|
d9a99155d9 | ||
|
|
9a1a181ecd | ||
|
|
afc67b0582 | ||
|
|
c049bbd5ac | ||
|
|
2dbbc9713c | ||
|
|
05c5f105e9 | ||
|
|
90b62b44e4 | ||
|
|
5657f1e673 | ||
|
|
10a82f8e85 | ||
|
|
75c599b5b3 | ||
|
|
8929a17c97 | ||
|
|
bd5cd9393a | ||
|
|
91a20cb034 | ||
|
|
9cc29eec35 | ||
|
|
24bc74fc87 | ||
|
|
164e0d1437 | ||
|
|
90e0e2d594 | ||
|
|
df418cde9c | ||
|
|
baf0d1fc06 | ||
|
|
b558f46d7a | ||
|
|
e2ac5a6325 | ||
|
|
e7098e3777 | ||
|
|
d1d20a4067 | ||
|
|
aa6929cd50 | ||
|
|
fb32bb3c39 | ||
|
|
a3db0c5500 | ||
|
|
376fa5e8af | ||
|
|
38f4be1e04 | ||
|
|
322d5ea64d | ||
|
|
a2e4ec867c | ||
|
|
a47b35df88 | ||
|
|
e4f2280625 | ||
|
|
4134603ec6 | ||
|
|
937ddd0a97 | ||
|
|
1e37d71878 | ||
|
|
ed8e3327b4 | ||
|
|
15ecbcc7de | ||
|
|
ae41e15c1b | ||
|
|
9538ef2ab7 | ||
|
|
c07c87718b | ||
|
|
d74652373c | ||
|
|
70e56d6620 | ||
|
|
65d8468520 | ||
|
|
3a39abe9c6 | ||
|
|
4e1fdd6a22 | ||
|
|
30dba8fee3 | ||
|
|
42e315f2f3 | ||
|
|
5ed58b1316 | ||
|
|
47f806d112 | ||
|
|
001c86b724 | ||
|
|
5bd32c61c2 | ||
|
|
23649b2c20 | ||
|
|
49b8b6d301 | ||
|
|
faad50b1df | ||
|
|
0d44de2ea7 | ||
|
|
b9985bff3b | ||
|
|
499a0ba5ab | ||
|
|
c59ff550a7 | ||
|
|
e568de2379 | ||
|
|
af09d1ae86 | ||
|
|
7720138290 | ||
|
|
3b4862e7a6 | ||
|
|
5e007894ea | ||
|
|
7f3bfff542 | ||
|
|
4c71aa9db1 | ||
|
|
bd09013d85 | ||
|
|
f091748542 | ||
|
|
9cad3fc4e0 | ||
|
|
bef7c994ba | ||
|
|
a29a8ddec2 | ||
|
|
46bc05ab29 | ||
|
|
baa75334ea | ||
|
|
6430de9c5d | ||
|
|
b0130f39d5 | ||
|
|
ed8b1b71c1 | ||
|
|
fc620d668f | ||
|
|
6792e0e79a | ||
|
|
69350bb79e | ||
|
|
ab2ebcd56d | ||
|
|
f58463c0d4 | ||
|
|
8992132d13 | ||
|
|
939ef29800 | ||
|
|
c357773647 | ||
|
|
b8ce414f11 | ||
|
|
1fa1ae848d | ||
|
|
0961dc43e3 | ||
|
|
ce279cd992 | ||
|
|
d360a85963 | ||
|
|
d16e079725 | ||
|
|
8cd8c32099 | ||
|
|
994cbb44b8 | ||
|
|
99ef152434 | ||
|
|
36f7aae476 | ||
|
|
f35af82bec | ||
|
|
f3ada66c11 | ||
|
|
18fbfcf3cc | ||
|
|
fd1927f30b | ||
|
|
68a6f0f0f2 | ||
|
|
86d28f47d1 | ||
|
|
2fd56f9dbf | ||
|
|
c999ac4c8b | ||
|
|
611b284ade | ||
|
|
b48578a7ea | ||
|
|
9315fcfa17 | ||
|
|
b9739f7b4e | ||
|
|
90f653d3ee | ||
|
|
0cff02158b | ||
|
|
d8beb39e9a | ||
|
|
25e853fa8c | ||
|
|
84ff2d16c7 | ||
|
|
09da1dc253 | ||
|
|
ca7eb04f6e | ||
|
|
54e902d1e3 | ||
|
|
7b77c4c9a3 | ||
|
|
f803188a4d | ||
|
|
53cf3191c2 | ||
|
|
c252d6b5f9 | ||
|
|
3579b39933 | ||
|
|
caaae427d5 | ||
|
|
c154302af4 | ||
|
|
3b1aa450df | ||
|
|
b34b5c5d26 | ||
|
|
c4ea26e443 | ||
|
|
070ee70949 | ||
|
|
e280305728 | ||
|
|
4c98cdf543 | ||
|
|
8bc6802251 | ||
|
|
a56f19bd4c | ||
|
|
069d961585 | ||
|
|
f6570c7e40 | ||
|
|
aa2c8cfb42 | ||
|
|
6c75b106b3 | ||
|
|
a8cbe99873 | ||
|
|
986c7f7b83 | ||
|
|
73d7f7f062 | ||
|
|
3dd2d40c50 | ||
|
|
042a48088d | ||
|
|
ecd63bdea5 | ||
|
|
e9b9443c06 | ||
|
|
53b33dfbc5 | ||
|
|
d367f9d1d4 | ||
|
|
54a4876beb | ||
|
|
14009f45d6 | ||
|
|
bd3d7eac50 | ||
|
|
b9ae17234d | ||
|
|
a76a7e680e | ||
|
|
2d4ec0e5ba | ||
|
|
bf2325e2ef | ||
|
|
de8ce9fc81 | ||
|
|
02629b6f6c | ||
|
|
fe8e2786c2 | ||
|
|
bbaa71f4b2 | ||
|
|
2dab20653f | ||
|
|
7bb0ef856a | ||
|
|
a192ffa6bc | ||
|
|
4bb7477147 | ||
|
|
ef7595a218 | ||
|
|
87ad01bea9 | ||
|
|
2461ed1aa4 | ||
|
|
77bb7a7112 | ||
|
|
1645413f8d | ||
|
|
26d06c50af | ||
|
|
17103cbc9a | ||
|
|
24e7f2cd17 | ||
|
|
a4b09a77c3 | ||
|
|
002c0e76c3 | ||
|
|
eb5ea901f4 | ||
|
|
03d93a2fba | ||
|
|
3833fd3884 | ||
|
|
8efaab48fd | ||
|
|
b80b017d33 | ||
|
|
2259093790 | ||
|
|
10e1126cd7 | ||
|
|
7e3cfa5875 | ||
|
|
b1ca686e06 | ||
|
|
2e7215946b | ||
|
|
ab5ad94d65 | ||
|
|
f86dc09a9e | ||
|
|
8fdaf91d34 | ||
|
|
aa53001982 | ||
|
|
4751594ee8 | ||
|
|
d14dbde697 | ||
|
|
55a5534777 | ||
|
|
a48619dde5 | ||
|
|
bf79c0fd6a | ||
|
|
5874ae270f | ||
|
|
fc8fc1ed8d | ||
|
|
98c4caac68 | ||
|
|
a159838d96 | ||
|
|
3f59f1a353 | ||
|
|
fb78147035 | ||
|
|
10af34fdad | ||
|
|
aefef6a456 | ||
|
|
536a0c45c8 | ||
|
|
3ddecce241 | ||
|
|
dffd6a63a6 | ||
|
|
0aaa4b3ddd | ||
|
|
1cbe5d60e7 | ||
|
|
907517595c | ||
|
|
0c7fd18bc9 | ||
|
|
5caa9a1e4f | ||
|
|
103bc0c232 | ||
|
|
834c85f0f1 | ||
|
|
8713f992a1 | ||
|
|
ed40364f09 | ||
|
|
57311aaa2e | ||
|
|
d4551a8c35 | ||
|
|
6d387f847e | ||
|
|
33ffc5eaac | ||
|
|
7b8251214b | ||
|
|
bbd155b917 | ||
|
|
30a3c8bc5a | ||
|
|
79d4343cdc | ||
|
|
dab4862f28 | ||
|
|
47bc680889 | ||
|
|
ae06d18aa2 | ||
|
|
75dd3af9bd | ||
|
|
6b2ca1d510 | ||
|
|
ae534a2e1e | ||
|
|
49c15e26d3 | ||
|
|
911c1d7ec2 | ||
|
|
de4617cd6b | ||
|
|
7a12aa44eb | ||
|
|
6758483ab2 | ||
|
|
74ede45d92 | ||
|
|
9b344d3753 | ||
|
|
3656e43d3c | ||
|
|
f1a5b90ca5 | ||
|
|
49f8de8661 | ||
|
|
bc16ef6860 | ||
|
|
e7d85133c3 | ||
|
|
f434d88f29 | ||
|
|
d545093124 | ||
|
|
835e816b04 | ||
|
|
9cda69c23f | ||
|
|
bcfa601efc | ||
|
|
01c8c04df6 | ||
|
|
24f83c0284 | ||
|
|
8bc3fd3cb7 | ||
|
|
9485c510c0 | ||
|
|
b6ec530c68 | ||
|
|
aad5a1b360 | ||
|
|
2292de332f | ||
|
|
8dec37a474 | ||
|
|
ad43fc8173 | ||
|
|
bc2f222036 | ||
|
|
94ac183131 | ||
|
|
f8b948721f | ||
|
|
407d0578ca | ||
|
|
741b938587 | ||
|
|
a210d653d2 | ||
|
|
e4ad1745d4 | ||
|
|
bfafd5789d | ||
|
|
21ae004979 | ||
|
|
de297c9904 | ||
|
|
fcba504618 | ||
|
|
3157c4d41a | ||
|
|
de711bec7a | ||
|
|
1e71ebbd44 | ||
|
|
7ceb2fb3d6 | ||
|
|
d27112b5a8 | ||
|
|
e2bfab5131 | ||
|
|
380796875f | ||
|
|
ae22e51868 | ||
|
|
c921825007 | ||
|
|
0485fbca43 | ||
|
|
6c61059cfe | ||
|
|
0091e3a8dd | ||
|
|
dee5278f52 | ||
|
|
f7e5880092 | ||
|
|
a3244549f3 | ||
|
|
6af88365c2 | ||
|
|
dc4835f14c | ||
|
|
7c1540ff6d | ||
|
|
5e9ac0bef5 | ||
|
|
c00a796203 | ||
|
|
931809edc4 | ||
|
|
813eb4c3cd | ||
|
|
8a072bd028 | ||
|
|
0a2ec3af08 | ||
|
|
41714fca0b | ||
|
|
f1c162d10f | ||
|
|
fa6132a7d1 | ||
|
|
6c26e448fd | ||
|
|
b2189d9501 | ||
|
|
571c941ae8 | ||
|
|
05a1900d60 | ||
|
|
3ac8ab2086 | ||
|
|
0f1adffdd5 | ||
|
|
a5627e6ba1 | ||
|
|
62b7c33d33 | ||
|
|
68ff96ae84 | ||
|
|
467890a60b | ||
|
|
25c9ecdad6 | ||
|
|
b260fff8e8 | ||
|
|
50a19b2ff9 | ||
|
|
215ce98c22 | ||
|
|
eea2e8777f | ||
|
|
5e1204ab2f | ||
|
|
fc5ddcb3f4 | ||
|
|
6b316b868c | ||
|
|
4fff047c4c | ||
|
|
c78e8e13c3 | ||
|
|
75cfc7bcb1 | ||
|
|
84768e3406 | ||
|
|
fc0842e388 | ||
|
|
36cbb5bf81 | ||
|
|
fc47b7753f | ||
|
|
058bae7446 | ||
|
|
029c2b8c6f | ||
|
|
a5e378073c | ||
|
|
a942032bf0 | ||
|
|
57ef70911b | ||
|
|
5c3ffc9c32 | ||
|
|
9cf80062a3 | ||
|
|
cb0e5b0645 | ||
|
|
b00d81bf63 | ||
|
|
e9915f481e | ||
|
|
236965cc63 | ||
|
|
91b938fd54 | ||
|
|
443077bdc3 | ||
|
|
932e199622 | ||
|
|
3e4b091724 | ||
|
|
1dc75b529d | ||
|
|
790bc21034 | ||
|
|
f16838a916 | ||
|
|
4e313f02c7 | ||
|
|
ae764c946a | ||
|
|
d159944d37 | ||
|
|
e81dc698dd | ||
|
|
31331cccb5 | ||
|
|
9011bdbb8a | ||
|
|
f404a92387 | ||
|
|
379e3c8ce6 | ||
|
|
31d9eb3f9e | ||
|
|
b536f0974e | ||
|
|
a23662baba | ||
|
|
ff739c430e | ||
|
|
fc9697926c | ||
|
|
8e3ed21a3a | ||
|
|
f6b583575a | ||
|
|
922f7c3622 | ||
|
|
aeeac8b2ed | ||
|
|
53b18cfd0c | ||
|
|
58440d5993 | ||
|
|
6a65d74ccf | ||
|
|
b52dd783b3 | ||
|
|
7996e19900 | ||
|
|
1a6870de69 | ||
|
|
2e6d85dfc2 | ||
|
|
8f937d5610 | ||
|
|
16b6adf1a1 | ||
|
|
91c04b1d5e | ||
|
|
5efbb268a5 | ||
|
|
79c5dfbdcb | ||
|
|
f4438b9000 | ||
|
|
4a5a7727b5 | ||
|
|
1b6ef07ef8 | ||
|
|
29d0552b9f | ||
|
|
b483c30109 | ||
|
|
70db1ee68b | ||
|
|
eee65f0f55 | ||
|
|
6b1a2c6f99 | ||
|
|
756edc1cdd | ||
|
|
ac0318b3f4 | ||
|
|
2eea724727 | ||
|
|
3fb32af89f | ||
|
|
d58c47fd27 | ||
|
|
b67dd576e5 | ||
|
|
deeb1ccc38 | ||
|
|
3d7682732c | ||
|
|
c26ecff9f2 | ||
|
|
3d110af911 | ||
|
|
e1f910848f | ||
|
|
3b271e7c41 | ||
|
|
7a5d7be255 | ||
|
|
05d427cbea | ||
|
|
0e3269b97e | ||
|
|
50fd54c6d9 | ||
|
|
180d735706 | ||
|
|
24c413030f | ||
|
|
06b77d598e | ||
|
|
7cdf7a0890 | ||
|
|
e5c75f7359 | ||
|
|
d258274322 | ||
|
|
58157e2d1c | ||
|
|
e47ada7e58 | ||
|
|
ef05dff851 | ||
|
|
a0e0822b5a | ||
|
|
dca4d4ffca | ||
|
|
008aa97675 | ||
|
|
aa43a2eec9 | ||
|
|
7255ef0669 | ||
|
|
2f756c77bb | ||
|
|
d0cf598ced | ||
|
|
b5feb85792 | ||
|
|
a6af5c8ca6 | ||
|
|
22a07ca213 | ||
|
|
c99923348a | ||
|
|
1f5fc2d254 | ||
|
|
64c3b50860 | ||
|
|
0f914eb9b8 | ||
|
|
7441a9a88f | ||
|
|
71a0ae2157 | ||
|
|
4359743b7b | ||
|
|
eefdc6ef71 | ||
|
|
1b930b5a19 | ||
|
|
519f2c69a5 | ||
|
|
cbfaef7fbb | ||
|
|
a5485de510 | ||
|
|
ab1445510a | ||
|
|
f514a4fde1 | ||
|
|
d17cd28c94 | ||
|
|
40bef8e70c | ||
|
|
fbd8e7dc42 | ||
|
|
b007f7c15e | ||
|
|
c790b68d47 | ||
|
|
89645c0f4c | ||
|
|
e4bb19ff60 | ||
|
|
ecb20ef99d | ||
|
|
2860bcfa5c | ||
|
|
ffe3ff18bf | ||
|
|
2edd1ba852 | ||
|
|
8988e84a01 | ||
|
|
2634e6517e | ||
|
|
0fcd948636 | ||
|
|
d17f3eccdb | ||
|
|
ba1483241c | ||
|
|
192c26871d | ||
|
|
659ca8692a | ||
|
|
ad77da1e86 | ||
|
|
ec56efb2b3 | ||
|
|
916fbcb674 | ||
|
|
8ae7009811 | ||
|
|
2b25ae6b35 | ||
|
|
af29b13ba4 | ||
|
|
97cc5d42a4 | ||
|
|
ecf971fe31 | ||
|
|
ca2d564e6a | ||
|
|
6c1f52a86c | ||
|
|
a991013040 | ||
|
|
dba116c57b | ||
|
|
0e432c2975 | ||
|
|
907a3cdbfe | ||
|
|
d83d76ca8e | ||
|
|
7d7cc0d174 | ||
|
|
9cf6ad8b88 | ||
|
|
1a0a2badd4 | ||
|
|
cdfbc45887 | ||
|
|
7b8276a387 | ||
|
|
460b92c044 | ||
|
|
6be35dc045 | ||
|
|
cc3be4a58b | ||
|
|
a7bfaf92df | ||
|
|
f7d367b7c1 | ||
|
|
7590943e9d | ||
|
|
2cf886d825 | ||
|
|
ce2c381116 | ||
|
|
ac3fb4d392 | ||
|
|
43b6e73970 | ||
|
|
d312dfc791 | ||
|
|
6316b393af | ||
|
|
356d50529c | ||
|
|
acbb30a9b1 | ||
|
|
0c3f56d7bb | ||
|
|
bc88d58e59 | ||
|
|
44d937b8bc | ||
|
|
99ef8fafce | ||
|
|
3947fbce4b | ||
|
|
637a49e274 | ||
|
|
a0d4567d3f |
16
.cnb.yml
16
.cnb.yml
@@ -1,3 +1,16 @@
|
|||||||
|
$:
|
||||||
|
vscode:
|
||||||
|
- runner:
|
||||||
|
cpus: 6
|
||||||
|
docker:
|
||||||
|
build: .ide/Dockerfile
|
||||||
|
services:
|
||||||
|
- vscode
|
||||||
|
- docker
|
||||||
|
stages:
|
||||||
|
- name: ls
|
||||||
|
script: ls -al
|
||||||
|
|
||||||
main:
|
main:
|
||||||
push:
|
push:
|
||||||
- runner:
|
- runner:
|
||||||
@@ -14,5 +27,4 @@ main:
|
|||||||
username: ${GIT_USERNAME}
|
username: ${GIT_USERNAME}
|
||||||
password: ${GIT_ACCESS_TOKEN}
|
password: ${GIT_ACCESS_TOKEN}
|
||||||
force: true
|
force: true
|
||||||
|
sync_mode: push
|
||||||
#sync_mode: rebase
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -43,6 +43,8 @@ logic/logic1
|
|||||||
logic/logic1
|
logic/logic1
|
||||||
public/logic-linux-amd64
|
public/logic-linux-amd64
|
||||||
public/login-linux-amd64
|
public/login-linux-amd64
|
||||||
|
.cache/gomod/**
|
||||||
public/login-login-linux-amd64
|
public/login-login-linux-amd64
|
||||||
public/logic_linux-amd64_1
|
public/logic_linux-amd64_1
|
||||||
|
.cache/**
|
||||||
|
.agents/**
|
||||||
|
|||||||
158
.ide/Dockerfile
158
.ide/Dockerfile
@@ -1,45 +1,135 @@
|
|||||||
# 此文件为远程开发环境配置文件
|
# 此文件为远程开发环境配置文件
|
||||||
FROM debian:bookworm
|
FROM debian:bookworm
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 1. 基础环境变量
|
||||||
|
# ==========================================
|
||||||
ENV GO_VERSION=1.25.0
|
ENV GO_VERSION=1.25.0
|
||||||
|
ENV GOPATH=/root/go
|
||||||
|
ENV PATH=/usr/local/go/bin:${GOPATH}/bin:${PATH}
|
||||||
|
ENV LC_ALL=zh_CN.UTF-8
|
||||||
|
ENV LANG=zh_CN.UTF-8
|
||||||
|
ENV LANGUAGE=zh_CN.UTF-8
|
||||||
|
ENV XDG_DATA_HOME=/var/lib
|
||||||
|
ENV XDG_CACHE_HOME=/workspace/.cache
|
||||||
|
ENV GOCACHE=/workspace/.cache/go-build
|
||||||
|
ENV GOMODCACHE=/workspace/.cache/gomod
|
||||||
|
|
||||||
RUN apt update &&\
|
# ==========================================
|
||||||
apt install -y wget rsync unzip openssh-server vim lsof git git-lfs locales locales-all libgit2-1.5 libgit2-dev net-tools jq curl &&\
|
# 2. Codex 配置 (更换时修改这里,重新 build)
|
||||||
rm -rf /var/lib/apt/lists/*
|
# ==========================================
|
||||||
|
ENV CODEX_BASE_URL="https://api.jucode.cn/v1"
|
||||||
|
|
||||||
# install golang
|
ENV CODEX_MODEL="gpt-5.4"
|
||||||
RUN curl -fsSLO https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz &&\
|
|
||||||
rm -rf /usr/local/go && tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz &&\
|
|
||||||
ln -sf /usr/local/go/bin/go /usr/bin/go &&\
|
|
||||||
ln -sf /usr/local/go/bin/gofmt /usr/bin/gofmt &&\
|
|
||||||
curl -sSfL https://raw.github.com/golangci/golangci-lint/master/install.sh | sh -s v1.54.2 &&\
|
|
||||||
rm -rf go${GO_VERSION}.linux-amd64.tar.gz
|
|
||||||
|
|
||||||
# install code-server
|
ENV OPENAI_API_KEY="sk-E0ZZIFNnD0RkhMC9pT2AGMutz9vNy2VLNrgyyobT5voa81pQ"
|
||||||
RUN curl -fsSL https://code-server.dev/install.sh | sh
|
|
||||||
RUN code-server --install-extension dbaeumer.vscode-eslint &&\
|
|
||||||
code-server --install-extension pinage404.git-extension-pack &&\
|
|
||||||
code-server --install-extension redhat.vscode-yaml &&\
|
|
||||||
code-server --install-extension esbenp.prettier-vscode &&\
|
|
||||||
code-server --install-extension golang.go &&\
|
|
||||||
code-server --install-extension eamodio.gitlens &&\
|
|
||||||
code-server --install-extension waderyan.gitblame &&\
|
|
||||||
code-server --install-extension donjayamanne.githistory &&\
|
|
||||||
code-server --install-extension mhutchie.git-graph &&\
|
|
||||||
code-server --install-extension ms-azuretools.vscode-docker &&\
|
|
||||||
code-server --install-extension PKief.material-icon-theme &&\
|
|
||||||
code-server --install-extension tencent-cloud.coding-copilot &&\
|
|
||||||
echo done
|
|
||||||
|
|
||||||
# install Go Tools
|
# ==========================================
|
||||||
ENV GOPATH /root/go
|
# 3. 安装系统依赖、Golang、Code-server
|
||||||
ENV PATH="${PATH}:${GOPATH}/bin"
|
# ==========================================
|
||||||
|
RUN set -ex; \
|
||||||
|
apt update && \
|
||||||
|
apt install -y --no-install-recommends \
|
||||||
|
wget rsync unzip openssh-server vim lsof git git-lfs \
|
||||||
|
locales libgit2-1.5 libgit2-dev net-tools jq curl ca-certificates sudo gnupg lsb-release xz-utils && \
|
||||||
|
curl -fsSLO "https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz" && \
|
||||||
|
rm -rf /usr/local/go && tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz" && \
|
||||||
|
rm -f "go${GO_VERSION}.linux-amd64.tar.gz" && \
|
||||||
|
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.54.2 && \
|
||||||
|
curl -fsSL https://code-server.dev/install.sh | sh && \
|
||||||
|
apt clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN go install -v golang.org/x/tools/gopls@latest
|
# ==========================================
|
||||||
|
# 4. 安装工具链 (国内加速版)
|
||||||
|
# ==========================================
|
||||||
|
RUN set -ex; \
|
||||||
|
go install -v golang.org/x/tools/gopls@latest && \
|
||||||
|
go install -v github.com/cweill/gotests/gotests@latest && \
|
||||||
|
go install -v github.com/josharian/impl@latest && \
|
||||||
|
go install -v github.com/haya14busa/goplay/cmd/goplay@latest && \
|
||||||
|
go install -v github.com/go-delve/delve/cmd/dlv@latest && \
|
||||||
|
go install github.com/goreleaser/goreleaser/v2@latest && \
|
||||||
|
wget -q "https://npmmirror.com/mirrors/node/v22.11.0/node-v22.11.0-linux-x64.tar.xz" -O /tmp/node.tar.xz && \
|
||||||
|
tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 && \
|
||||||
|
rm -f /tmp/node.tar.xz && \
|
||||||
|
npm config set registry https://registry.npmmirror.com/ && \
|
||||||
|
npm install -g @openai/codex
|
||||||
|
|
||||||
# install goreleaser
|
# ==========================================
|
||||||
RUN go install github.com/goreleaser/goreleaser/v2@latest
|
# 5. 生成 Codex 配置文件 (独立 RUN 块,彻底规避格式报错)
|
||||||
|
# ==========================================
|
||||||
|
RUN mkdir -p /root/.codex
|
||||||
|
|
||||||
ENV LC_ALL zh_CN.UTF-8
|
RUN cat > /root/.codex/config.toml <<EOF
|
||||||
ENV LANG zh_CN.UTF-8
|
model_provider = "OpenAI"
|
||||||
ENV LANGUAGE zh_CN.UTF-8
|
model = "${CODEX_MODEL}"
|
||||||
|
model_reasoning_effort = "high"
|
||||||
|
disable_response_storage = true
|
||||||
|
|
||||||
|
[model_providers.OpenAI]
|
||||||
|
name = "OpenAI"
|
||||||
|
base_url = "${CODEX_BASE_URL}"
|
||||||
|
wire_api = "responses"
|
||||||
|
requires_openai_auth = true
|
||||||
|
# 自动压缩触发阈值(token数)
|
||||||
|
model_auto_compact_token_limit = 100000 # 超过此值自动压缩
|
||||||
|
# 上下文窗口大小(根据模型调整)
|
||||||
|
model_context_window = 128000
|
||||||
|
# 压缩后保留的最小上下文
|
||||||
|
model_compact_min_keep_tokens = 20000
|
||||||
|
# 自动压缩开关(默认true)
|
||||||
|
model_auto_compact = true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RUN cat > /root/.codex/auth.json <<EOF
|
||||||
|
{
|
||||||
|
"auth_mode": "apikey",
|
||||||
|
"OPENAI_API_KEY": "${OPENAI_API_KEY}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RUN chmod 600 /root/.codex/auth.json && \
|
||||||
|
echo "export OPENAI_API_KEY=\"${OPENAI_API_KEY}\"" >> /root/.bashrc && \
|
||||||
|
echo "export CODEX_API_KEY=\"${OPENAI_API_KEY}\"" >> /root/.bashrc
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 6. 安装 code-server 插件
|
||||||
|
# ==========================================
|
||||||
|
RUN set -eux; \
|
||||||
|
USER_DATA_DIR=/var/lib/code-server; \
|
||||||
|
EXTENSIONS_DIR="${USER_DATA_DIR}/extensions"; \
|
||||||
|
mkdir -p "${EXTENSIONS_DIR}" /root/.vscode-server; \
|
||||||
|
FAILED_EXTENSIONS=""; \
|
||||||
|
for ext in \
|
||||||
|
dbaeumer.vscode-eslint \
|
||||||
|
redhat.vscode-yaml \
|
||||||
|
esbenp.prettier-vscode \
|
||||||
|
golang.go \
|
||||||
|
eamodio.gitlens \
|
||||||
|
waderyan.gitblame \
|
||||||
|
donjayamanne.githistory \
|
||||||
|
mhutchie.git-graph \
|
||||||
|
tencent-cloud.coding-copilot\
|
||||||
|
; do \
|
||||||
|
if ! /usr/bin/code-server --install-extension "${ext}" --user-data-dir "${USER_DATA_DIR}" --extensions-dir "${EXTENSIONS_DIR}"; then \
|
||||||
|
FAILED_EXTENSIONS="${FAILED_EXTENSIONS} ${ext}"; \
|
||||||
|
echo "WARN: extension install failed: ${ext}"; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
rm -rf /root/.vscode-server/extensions /root/extensions; \
|
||||||
|
ln -s "${EXTENSIONS_DIR}" /root/.vscode-server/extensions; \
|
||||||
|
ln -s "${EXTENSIONS_DIR}" /root/extensions; \
|
||||||
|
chmod -R a+rwX "${USER_DATA_DIR}"; \
|
||||||
|
chmod -R a+rX /root/.vscode-server; \
|
||||||
|
[ -z "${FAILED_EXTENSIONS}" ] && echo "所有插件安装完成 ✅" || echo "以下插件安装失败:${FAILED_EXTENSIONS}"
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 7. 统一缓存目录 & 环境变量
|
||||||
|
# ==========================================
|
||||||
|
RUN mkdir -p /workspace/.cache/go-build /workspace/.cache/gomod /workspace/.cache/goimports && \
|
||||||
|
chmod -R a+rwx /workspace/.cache && \
|
||||||
|
printf '%s\n' \
|
||||||
|
'export XDG_CACHE_HOME=/workspace/.cache' \
|
||||||
|
'export GOCACHE=/workspace/.cache/go-build' \
|
||||||
|
'export GOMODCACHE=/workspace/.cache/gomod' \
|
||||||
|
>> /etc/profile
|
||||||
30
.ide/help.md
Normal file
30
.ide/help.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
|
||||||
|
https://api.jucode.cn/
|
||||||
|
fastai.fast 使用谷歌邮箱https://linshiguge.com/白嫖
|
||||||
|
https://zread.ai/tawer-blog/lmarena-2api/1-overview GLM web2 pai
|
||||||
|
|
||||||
|
https://crazyrouter.com/console 模型最便宜,看看能不能1:10
|
||||||
|
|
||||||
|
https://agentrouter.org/pricing 签到给,有175
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
充了十块
|
||||||
|
使用网址:https://www.jnm.lol
|
||||||
|
使用文档:https://fcnkhhtxb5iz.feishu.cn/docx/VyhcdKduJoNCK4x4l25ci10JnZf。
|
||||||
|
24小时自助faka点luckwk点cn
|
||||||
|
不要发违禁词,看到会回消息,长期稳定使用。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fastai.fast 575560454@qq.com 575560454
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -29,7 +29,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"args": ["-id=2"],
|
"args": ["-id=99"],
|
||||||
|
|
||||||
"program": "${workspaceFolder}/logic"
|
"program": "${workspaceFolder}/logic"
|
||||||
}
|
}
|
||||||
|
|||||||
25
.vscode/settings.json
vendored
25
.vscode/settings.json
vendored
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"goBuild.savedBuildConfig": {
|
|
||||||
"outputName": "",
|
|
||||||
"outputDir": "./public",
|
|
||||||
"zcliEnabled": false,
|
|
||||||
"targetOS": "linux",
|
|
||||||
"targetArch": "current",
|
|
||||||
"enableRace": false,
|
|
||||||
"enableOptimization": true,
|
|
||||||
"stripSymbols": true,
|
|
||||||
"cgoEnabled": false,
|
|
||||||
"buildTags": "",
|
|
||||||
"customLdflags": "-X main.Version={{.Version}} -X main.BuildTime={{.BuildTime}} -X main.GitCommitID={{.GitCommit}} -X main.GitBranch={{.GitBranch}} -buildid= -extldflags '-static'",
|
|
||||||
"verboseMode": false,
|
|
||||||
"printCommands": false,
|
|
||||||
"keepWorkDir": false,
|
|
||||||
"forceRebuild": false,
|
|
||||||
"dryRun": false,
|
|
||||||
"trimPath": true,
|
|
||||||
"currentPreset": "production"
|
|
||||||
},
|
|
||||||
"go.toolsEnvVars": {},
|
|
||||||
"goBuild.zcli.enabled": false,
|
|
||||||
"cSpell.words": ["struc"]
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ skip_clone: true
|
|||||||
steps:
|
steps:
|
||||||
# ========== 1. 替代clone:拉取代码(核心依赖) ==========
|
# ========== 1. 替代clone:拉取代码(核心依赖) ==========
|
||||||
prepare:
|
prepare:
|
||||||
image: alpine/git
|
image: docker.1ms.run/alpine/git
|
||||||
environment:
|
environment:
|
||||||
# WOODPECKER_SSH_KEY:
|
# WOODPECKER_SSH_KEY:
|
||||||
# from_secret: WOODPECKER_SSH_KEY
|
# from_secret: WOODPECKER_SSH_KEY
|
||||||
@@ -56,9 +56,10 @@ steps:
|
|||||||
# - ssh-keyscan -H github.com > /root/.ssh/known_hosts
|
# - ssh-keyscan -H github.com > /root/.ssh/known_hosts
|
||||||
# - chmod 600 /root/.ssh/known_hosts
|
# - chmod 600 /root/.ssh/known_hosts
|
||||||
# - echo "🔍 ${#CI_REPO_CLONE_SSH_URL}调试: ${CI_REPO_CLONE_SSH_URL}"
|
# - echo "🔍 ${#CI_REPO_CLONE_SSH_URL}调试: ${CI_REPO_CLONE_SSH_URL}"
|
||||||
|
|
||||||
- git config --global core.compression 0
|
- git config --global core.compression 0
|
||||||
- export GIT_CONFIG_URL="https://cnb:$CNB_ACCK@cnb.cool/blzing/blazing"
|
- export GIT_CONFIG_URL="https://cnb:$CNB_ACCK@cnb.cool/blzing/blazing"
|
||||||
- echo "🔍 $CNB_ACCK调试: $GIT_CONFIG_URL"
|
- echo "🔍 $CNB_ACCK调试: $CNB_ACCK"
|
||||||
- git config --global http.sslVerify false
|
- git config --global http.sslVerify false
|
||||||
- git clone --depth 1 --progress -v $GIT_CONFIG_URL
|
- git clone --depth 1 --progress -v $GIT_CONFIG_URL
|
||||||
# 拉取代码
|
# 拉取代码
|
||||||
@@ -69,13 +70,28 @@ steps:
|
|||||||
|
|
||||||
# ========== 4. 编译Logic服务(完全参考GitHub Actions编译配置) ==========
|
# ========== 4. 编译Logic服务(完全参考GitHub Actions编译配置) ==========
|
||||||
build_logic:
|
build_logic:
|
||||||
image: golang:1.25
|
image: docker.m.daocloud.io/golang:1.25
|
||||||
depends_on: [prepare]
|
depends_on: [prepare]
|
||||||
environment:
|
environment:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GO111MODULE: on
|
GO111MODULE: on
|
||||||
GOSUMDB: off
|
GOSUMDB: off
|
||||||
commands:
|
commands:
|
||||||
|
# 2. 清空主源文件(关键:先删空,再写入)
|
||||||
|
- >
|
||||||
|
echo "" > /etc/apt/sources.list
|
||||||
|
# 3. 写入阿里云trixie源(匹配golang:1.25的系统版本,避免版本混跑)
|
||||||
|
- >
|
||||||
|
echo "deb http://mirrors.aliyun.com/debian/ trixie main contrib non-free non-free-firmware
|
||||||
|
deb http://mirrors.aliyun.com/debian/ trixie-updates main contrib non-free non-free-firmware
|
||||||
|
deb http://mirrors.aliyun.com/debian-security/ trixie-security main contrib non-free non-free-firmware" > /etc/apt/sources.list
|
||||||
|
# 4. 删除sources.list.d下的所有额外源(彻底杜绝官方源)
|
||||||
|
- rm -rf /etc/apt/sources.list.d/*
|
||||||
|
# 5. 强制更新,加超时和缓存清理(解决卡住问题)
|
||||||
|
- apt-get clean && apt-get update -y -o Acquire::Timeout=30
|
||||||
|
# 2. 安装正确的 upx 包(Debian 中包名是 upx-ucl,不是 upx)
|
||||||
|
- apt-get install -y upx-ucl
|
||||||
|
|
||||||
- cd blazing
|
- cd blazing
|
||||||
- mkdir -p build
|
- mkdir -p build
|
||||||
- BIN_NAME="login_${CI_PIPELINE_CREATED}"
|
- BIN_NAME="login_${CI_PIPELINE_CREATED}"
|
||||||
@@ -89,6 +105,9 @@ steps:
|
|||||||
-ldflags "-s -w -buildid= -extldflags '-static'" \
|
-ldflags "-s -w -buildid= -extldflags '-static'" \
|
||||||
-o ./build/$BIN_NAME \
|
-o ./build/$BIN_NAME \
|
||||||
./login
|
./login
|
||||||
|
# - |
|
||||||
|
# strip ./build/$BIN_NAME
|
||||||
|
# upx --best --lzma ./build/$BIN_NAME
|
||||||
- |
|
- |
|
||||||
if [ ! -f ./build/$BIN_NAME ]; then
|
if [ ! -f ./build/$BIN_NAME ]; then
|
||||||
echo "❌ 编译失败:产物$BIN_NAME不存在"
|
echo "❌ 编译失败:产物$BIN_NAME不存在"
|
||||||
@@ -105,6 +124,9 @@ steps:
|
|||||||
-ldflags "-s -w -buildid= -extldflags '-static'" \
|
-ldflags "-s -w -buildid= -extldflags '-static'" \
|
||||||
-o ./build/$BIN_NAME \
|
-o ./build/$BIN_NAME \
|
||||||
./logic
|
./logic
|
||||||
|
- |
|
||||||
|
strip ./build/$BIN_NAME
|
||||||
|
upx --best --lzma ./build/$BIN_NAME
|
||||||
- |
|
- |
|
||||||
if [ ! -f ./build/$BIN_NAME ]; then
|
if [ ! -f ./build/$BIN_NAME ]; then
|
||||||
echo "❌ 编译失败:产物$BIN_NAME不存在"
|
echo "❌ 编译失败:产物$BIN_NAME不存在"
|
||||||
@@ -120,23 +142,23 @@ steps:
|
|||||||
|
|
||||||
# ========== 6. SCP推送产物(依赖编译+配置解析) ==========
|
# ========== 6. SCP推送产物(依赖编译+配置解析) ==========
|
||||||
scp-exe-to-servers: # 与fetch-deploy-config同级,缩进2个空格
|
scp-exe-to-servers: # 与fetch-deploy-config同级,缩进2个空格
|
||||||
image: appleboy/drone-scp:1.6.2 # 子元素,缩进4个空格
|
image: docker.1ms.run/appleboy/drone-scp:1.6.2 # 子元素,缩进4个空格
|
||||||
settings: # 子元素,缩进4个空格
|
settings: # 子元素,缩进4个空格
|
||||||
host: &ssh_host 2697v22.mc5173.cn
|
host: &ssh_host 43.248.3.21
|
||||||
port: &ssh_port 16493
|
port: &ssh_port 22
|
||||||
username: &ssh_user root
|
username: &ssh_user root
|
||||||
password: &ssh_pass xIy9PQcBF96C
|
password: &ssh_pass KQv7yzna7BDukK
|
||||||
|
|
||||||
source:
|
source:
|
||||||
- blazing/build/**
|
- blazing/build/**
|
||||||
target: /opt/blazing/
|
target: /ext/blazing/
|
||||||
strip_components: 1 # 统一缩进6个空格
|
strip_components: 1 # 统一缩进6个空格
|
||||||
skip_verify: true # 统一缩进6个空格
|
skip_verify: true # 统一缩进6个空格
|
||||||
timeout: 30s # 统一缩进6个空格
|
timeout: 30s # 统一缩进6个空格
|
||||||
depends_on: # 子元素,缩进4个空格
|
depends_on: # 子元素,缩进4个空格
|
||||||
- build_logic # depends_on内的项,缩进6个空格
|
- build_logic # depends_on内的项,缩进6个空格
|
||||||
start-login-logic:
|
start-login-logic:
|
||||||
image: appleboy/drone-ssh:1.6.2
|
image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/ghcr.io/appleboy/drone-ssh:1.7.7
|
||||||
depends_on: [scp-exe-to-servers]
|
depends_on: [scp-exe-to-servers]
|
||||||
settings: # 子元素,缩进4个空格
|
settings: # 子元素,缩进4个空格
|
||||||
host: *ssh_host
|
host: *ssh_host
|
||||||
@@ -145,7 +167,7 @@ steps:
|
|||||||
password: *ssh_pass
|
password: *ssh_pass
|
||||||
script:
|
script:
|
||||||
- |
|
- |
|
||||||
cd /opt/blazing/build
|
cd /ext/blazing/build
|
||||||
ls -t login_* 2>/dev/null | head -1
|
ls -t login_* 2>/dev/null | head -1
|
||||||
BIN_NAME=$(ls -t login_* 2>/dev/null | head -1)
|
BIN_NAME=$(ls -t login_* 2>/dev/null | head -1)
|
||||||
echo "BIN_NAME: $BIN_NAME"
|
echo "BIN_NAME: $BIN_NAME"
|
||||||
@@ -179,9 +201,9 @@ steps:
|
|||||||
# 移动logic产物到public目录
|
# 移动logic产物到public目录
|
||||||
LOGIC_BIN=$(ls -t logic_* 2>/dev/null | head -1)
|
LOGIC_BIN=$(ls -t logic_* 2>/dev/null | head -1)
|
||||||
if [ -n "$LOGIC_BIN" ]; then
|
if [ -n "$LOGIC_BIN" ]; then
|
||||||
mkdir -p /opt/blazing/build/public
|
mkdir -p /ext/blazing/build/public
|
||||||
mv $LOGIC_BIN /opt/blazing/build/public/
|
mv $LOGIC_BIN /ext/blazing/build/public/
|
||||||
echo "✅ Logic产物已移动到 /opt/blazing/build/public/ | 文件: $(basename $LOGIC_BIN)"
|
echo "✅ Logic产物已移动到 /ext/blazing/build/public/ | 文件: $(basename $LOGIC_BIN)"
|
||||||
else
|
else
|
||||||
echo "⚠️ 未找到Logic产物"
|
echo "⚠️ 未找到Logic产物"
|
||||||
fi
|
fi
|
||||||
|
|||||||
93
AGENTS.md
Normal file
93
AGENTS.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
This repository is split into multiple Go modules:
|
||||||
|
|
||||||
|
- `logic/`: main game logic and fight system (`logic/service/fight/...`)
|
||||||
|
- `login/`: login service
|
||||||
|
- `common/`: shared utilities, data, RPC helpers, socket code
|
||||||
|
- `modules/`: domain modules such as `player`, `task`, `space`
|
||||||
|
- `public/`: runtime data and configs, including `public/config/*.json`
|
||||||
|
- `docs/`: engineering notes and feature-specific summaries
|
||||||
|
|
||||||
|
Keep changes scoped to the owning module. For example, fight effect work belongs under `logic/service/fight/effect/`.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
- `cd logic && go test ./service/fight/effect`
|
||||||
|
Validates effect package changes quickly.
|
||||||
|
- `cd logic && go test ./...`
|
||||||
|
Runs all tests in the `logic` module.
|
||||||
|
- `cd common && go test ./...`
|
||||||
|
Runs shared utility tests.
|
||||||
|
- `cd logic && go build ./...`
|
||||||
|
Checks compile health for the logic module.
|
||||||
|
|
||||||
|
CI currently builds Go artifacts through GitHub Actions in `.github/workflows/logic_CI.yml`.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
Use standard Go formatting and idioms:
|
||||||
|
|
||||||
|
- Run `gofmt -w <file>.go` on edited Go files.
|
||||||
|
- Use tabs as produced by `gofmt`; do not hand-align spacing.
|
||||||
|
- Keep package names lowercase.
|
||||||
|
- Follow existing effect naming: `Effect<ID>` structs in files like `effect_123.go` or grouped files such as `400_480_...go`.
|
||||||
|
- Keep comments short and descriptive, e.g. `// Effect 400: 若和对手属性相同,则技能威力翻倍`.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
The repo uses Go’s built-in `testing` package. Existing tests are sparse, so at minimum:
|
||||||
|
|
||||||
|
- run package-level tests for the module you changed
|
||||||
|
- prefer targeted verification first, then broader `go test ./...` when practical
|
||||||
|
- name tests with Go conventions, e.g. `TestSqrt`, `TestEffect400`
|
||||||
|
|
||||||
|
If no automated test exists, document the package-level command you used to validate the change.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
Recent history is inconsistent (`fix: ...`, `编辑文件 ...`, and short placeholder commits). Prefer clear messages:
|
||||||
|
|
||||||
|
- `fix: correct Effect599 damage reduction category handling`
|
||||||
|
- `docs: update effect refactor summary`
|
||||||
|
|
||||||
|
For pull requests, include:
|
||||||
|
|
||||||
|
- what changed
|
||||||
|
- affected module(s)
|
||||||
|
- validation commands run
|
||||||
|
- linked issue/task if available
|
||||||
|
|
||||||
|
## Contributor Notes
|
||||||
|
Do not overwrite unrelated local changes. This repo often has a dirty worktree. Prefer additive edits, and update `docs/` when continuing long-running refactors such as fight effects.
|
||||||
|
|
||||||
|
## Battle System Notes
|
||||||
|
Most combat work lives under `logic/service/fight/`. Use the existing split before adding code:
|
||||||
|
|
||||||
|
- `action/`: battle action types and turn execution helpers
|
||||||
|
- `input/`: runtime battle state, effect registration, skill parsing
|
||||||
|
- `info/`: core battle entities such as pets, skills, damage zones, enums
|
||||||
|
- `effect/`: skill effects and status logic; most day-to-day fight changes land here
|
||||||
|
- `node/`: shared effect node behavior and default hooks
|
||||||
|
- `boss/`: boss-only passive/index effects
|
||||||
|
- `rule/`, `itemover/`, `top/`: rules, item settlement, ranking-related battle logic
|
||||||
|
|
||||||
|
When adding a new skill effect:
|
||||||
|
|
||||||
|
- prefer `logic/service/fight/effect/`
|
||||||
|
- follow existing naming such as `Effect400` or grouped files like `400_480_...go`
|
||||||
|
- register via `input.InitEffect(...)` or existing helper registration paths
|
||||||
|
- update `effect_info_map.go` if the effect should appear in local effect descriptions
|
||||||
|
|
||||||
|
When investigating missing effects, do not rely only on direct `InitEffect(...)` grep results. This repo also uses shared registration files such as:
|
||||||
|
|
||||||
|
- `sterStatusEffects.go`
|
||||||
|
- `effect_power_doblue.go`
|
||||||
|
- `EffectAttackMiss.go`
|
||||||
|
- `EffectPhysicalAttackAddStatus.go`
|
||||||
|
- `EffectDefeatTrigger.go`
|
||||||
|
- `effect_attr.go`
|
||||||
|
|
||||||
|
Recommended validation for fight changes:
|
||||||
|
|
||||||
|
- `cd logic && go test ./service/fight/effect`
|
||||||
|
- `cd logic && go build ./...`
|
||||||
|
|
||||||
|
If you continue long-running effect work, update the matching summary in `docs/` so the next pass can resume without re-scanning the whole package.
|
||||||
@@ -8,11 +8,18 @@
|
|||||||
## seer-project
|
## seer-project
|
||||||
|
|
||||||
项目结构:
|
项目结构:
|
||||||
go tool pprof -http :8081 "http://125.208.20.223:54612/debug/debug/pprof/profile"
|
|
||||||
go tool pprof -http :8081 "http://127.0.0.1:9909/debug/pprof/profile"
|
go tool pprof -http :8081 "http://127.0.0.1:9909/debug/pprof/profile"
|
||||||
|
|
||||||
go tool pprof -http :8081 "http://202.189.15.67:62672/debug/pprof/profile"
|
go tool pprof -http :8081 "http://202.189.15.67:62672/debug/pprof/profile"
|
||||||
|
go tool pprof -http :8081 "http://8.162.8.203:9909/debug/pprof//profile"
|
||||||
|
go tool pprof -http :8081 "http://8.162.23.87:9910/debug/pprof//profile"
|
||||||
|
go tool pprof -http :8081 "http://61.147.247.7:36855/debug/pprof/profile"
|
||||||
|
go tool pprof -http :8081 "http://61.147.247.7:43892/debug/pprof/profile"
|
||||||
|
|
||||||
|
# 采样 60 秒的 CPU 数据,然后通过 HTTP 8081 端口可视化
|
||||||
|
|
||||||
|
go tool pprof -http :8081 "http://61.147.247.7:43892/debug/pprof/profile?seconds=300"
|
||||||
详情查看 [文档](./docs)
|
详情查看 [文档](./docs)
|
||||||
|
|
||||||
- [战斗](./docs/battle.md)
|
- [战斗](./docs/battle.md)
|
||||||
|
|||||||
56
common/contrib/drivers/pgsql/cmd/codexcheck/main.go
Normal file
56
common/contrib/drivers/pgsql/cmd/codexcheck/main.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
const dsn = "user=user_YrK4j7 password=password_jSDm76 host=43.248.3.21 port=5432 dbname=bl sslmode=disable timezone=Asia/Shanghai"
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", dsn)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
var (
|
||||||
|
id int64
|
||||||
|
cdkCode string
|
||||||
|
cdkType int64
|
||||||
|
exchangeRemainCount int64
|
||||||
|
bindUserID int64
|
||||||
|
validEndTime sql.NullTime
|
||||||
|
remark sql.NullString
|
||||||
|
)
|
||||||
|
|
||||||
|
err = db.QueryRow(`
|
||||||
|
select id, cdk_code, type, exchange_remain_count, bind_user_id, valid_end_time, remark
|
||||||
|
from config_gift_cdk
|
||||||
|
where cdk_code = $1
|
||||||
|
`, "nrTbdXFBhKkaTdDk").Scan(
|
||||||
|
&id,
|
||||||
|
&cdkCode,
|
||||||
|
&cdkType,
|
||||||
|
&exchangeRemainCount,
|
||||||
|
&bindUserID,
|
||||||
|
&validEndTime,
|
||||||
|
&remark,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("id=%d\ncdk_code=%s\ntype=%d\nexchange_remain_count=%d\nbind_user_id=%d\nvalid_end_time=%v\nremark=%q\n",
|
||||||
|
id,
|
||||||
|
cdkCode,
|
||||||
|
cdkType,
|
||||||
|
exchangeRemainCount,
|
||||||
|
bindUserID,
|
||||||
|
validEndTime.Time,
|
||||||
|
remark.String,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/net/ghttp"
|
||||||
"github.com/gogf/gf/v2/os/gfile"
|
"github.com/gogf/gf/v2/os/gfile"
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
)
|
)
|
||||||
@@ -17,21 +18,45 @@ type Local struct {
|
|||||||
func (l *Local) Upload(ctx g.Ctx) (string, error) {
|
func (l *Local) Upload(ctx g.Ctx) (string, error) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
Request = g.RequestFromCtx(ctx)
|
request = g.RequestFromCtx(ctx)
|
||||||
|
file *ghttp.UploadFile
|
||||||
)
|
)
|
||||||
|
|
||||||
file := Request.GetUploadFile("file")
|
// -------------------------- 核心兼容逻辑:适配PHP的字段名 --------------------------
|
||||||
|
// 优先级:uploadfiles(PHP转转适配器字段) > files(通用多文件字段) > file(原字段)
|
||||||
|
// 1. 先尝试获取PHP fof/upload插件的 uploadfiles 字段(单文件)
|
||||||
|
file = request.GetUploadFile("uploadfiles")
|
||||||
if file == nil {
|
if file == nil {
|
||||||
return "", gerror.New("上传文件为空")
|
// 2. 再尝试获取 files 字段(多文件取第一个,兼容PHP多文件上传)
|
||||||
|
files := request.GetUploadFiles("files")
|
||||||
|
if len(files) > 0 {
|
||||||
|
file = files[0] // 取第一个文件,和PHP Arr::get($data, 'data.0.url')逻辑一致
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 3. 最后兼容原 file 字段
|
||||||
|
if file == nil {
|
||||||
|
file = request.GetUploadFile("file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有字段都无文件,返回错误
|
||||||
|
if file == nil {
|
||||||
|
return "", gerror.New("上传文件为空(未找到file/files/uploadfiles字段)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------- 原有存储逻辑不变 --------------------------
|
||||||
// 以当前年月日为目录
|
// 以当前年月日为目录
|
||||||
dir := gtime.Now().Format("Ymd")
|
dir := gtime.Now().Format("Ymd")
|
||||||
|
// 保存路径:./public/uploads/年月日
|
||||||
fileName, err := file.Save("./public/uploads/"+dir, true)
|
saveDir := "./public/uploads/" + dir
|
||||||
|
// 保存文件(自动重命名避免重复)
|
||||||
|
fileName, err := file.Save(saveDir, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", gerror.Wrap(err, "保存上传文件失败")
|
||||||
}
|
}
|
||||||
return cool.Config.File.Domain + "/public/uploads/" + dir + "/" + fileName, err
|
|
||||||
|
// -------------------------- 拼接访问URL(原有逻辑不变) --------------------------
|
||||||
|
accessURL := "http://" + cool.Config.File.Domain + cool.Config.Address + "/uploads/" + dir + "/" + fileName
|
||||||
|
return accessURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Local) GetMode() (data interface{}, err error) {
|
func (l *Local) GetMode() (data interface{}, err error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cool
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"blazing/cool/coolconfig"
|
"blazing/cool/coolconfig"
|
||||||
@@ -98,6 +99,11 @@ func (c *Controller) Delete(ctx context.Context, req *DeleteReq) (res *BaseRes,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Fail(err.Error()), err
|
return Fail(err.Error()), err
|
||||||
}
|
}
|
||||||
|
t, _ := data.RowsAffected()
|
||||||
|
if t == 0 {
|
||||||
|
return Fail("not found"), err
|
||||||
|
}
|
||||||
|
|
||||||
c.Service.ModifyAfter(ctx, "Delete", g.RequestFromCtx(ctx).GetMap())
|
c.Service.ModifyAfter(ctx, "Delete", g.RequestFromCtx(ctx).GetMap())
|
||||||
return Ok(data), err
|
return Ok(data), err
|
||||||
}
|
}
|
||||||
@@ -115,6 +121,10 @@ func (c *Controller) Update(ctx context.Context, req *UpdateReq) (res *BaseRes,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Fail(err.Error()), err
|
return Fail(err.Error()), err
|
||||||
}
|
}
|
||||||
|
t, _ := data.RowsAffected()
|
||||||
|
if t == 0 {
|
||||||
|
return Fail("not found"), err
|
||||||
|
}
|
||||||
c.Service.ModifyAfter(ctx, "Update", g.RequestFromCtx(ctx).GetMap())
|
c.Service.ModifyAfter(ctx, "Update", g.RequestFromCtx(ctx).GetMap())
|
||||||
return Ok(data), err
|
return Ok(data), err
|
||||||
}
|
}
|
||||||
@@ -149,12 +159,33 @@ func (c *Controller) Page(ctx context.Context, req *PageReq) (res *BaseRes, err
|
|||||||
// 注册控制器到路由
|
// 注册控制器到路由
|
||||||
func RegisterController(c IController) {
|
func RegisterController(c IController) {
|
||||||
var ctx = context.Background()
|
var ctx = context.Background()
|
||||||
var sController = &Controller{}
|
var sController *Controller
|
||||||
gconv.Struct(c, &sController)
|
rv := reflect.ValueOf(c)
|
||||||
|
if rv.IsValid() && rv.Kind() == reflect.Ptr {
|
||||||
|
ev := rv.Elem()
|
||||||
|
if ev.IsValid() {
|
||||||
|
field := ev.FieldByName("Controller")
|
||||||
|
if field.IsValid() && !field.IsNil() {
|
||||||
|
if ctrl, ok := field.Interface().(*Controller); ok && ctrl != nil {
|
||||||
|
sController = ctrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sController == nil {
|
||||||
|
sController = &Controller{}
|
||||||
|
gconv.Struct(c, &sController)
|
||||||
|
}
|
||||||
if coolconfig.Config.Eps {
|
if coolconfig.Config.Eps {
|
||||||
model := sController.Service.GetModel()
|
model := sController.Service.GetModel()
|
||||||
columns := getModelInfo(ctx, sController.Prefix, model)
|
tableName := ""
|
||||||
ModelInfo[sController.Prefix] = columns
|
if model != nil {
|
||||||
|
tableName = strings.TrimSpace(model.TableName())
|
||||||
|
}
|
||||||
|
if tableName != "" && tableName != "this_table_should_not_exist" {
|
||||||
|
columns := getModelInfo(ctx, sController.Prefix, model)
|
||||||
|
ModelInfo[sController.Prefix] = columns
|
||||||
|
}
|
||||||
}
|
}
|
||||||
g.Server().Group(
|
g.Server().Group(
|
||||||
sController.Prefix, func(group *ghttp.RouterGroup) {
|
sController.Prefix, func(group *ghttp.RouterGroup) {
|
||||||
|
|||||||
@@ -95,3 +95,17 @@ func Fail(message string) *BaseRes {
|
|||||||
// }
|
// }
|
||||||
// return nil, nil
|
// return nil, nil
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
func RedisDo(ctx context.Context, funcstring string, a ...any) {
|
||||||
|
|
||||||
|
conn, err := Redis.Conn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close(ctx)
|
||||||
|
_, err = conn.Do(ctx, "publish", funcstring, a)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,120 +1,121 @@
|
|||||||
package coolconfig
|
package coolconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
)
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
// cool config
|
|
||||||
type sConfig struct {
|
// cool config
|
||||||
AutoMigrate bool `json:"auto_migrate,omitempty"` // 是否自动创建表
|
type sConfig struct {
|
||||||
Eps bool `json:"eps,omitempty"` // 是否开启eps
|
AutoMigrate bool `json:"auto_migrate,omitempty"` // 是否自动创建表
|
||||||
File *file `json:"file,omitempty"` // 文件上传配置
|
Eps bool `json:"eps,omitempty"` // 是否开启eps
|
||||||
Name string `json:"name"` // 项目名称
|
File *file `json:"file,omitempty"` // 文件上传配置
|
||||||
// LoginPort string `json:"port"`
|
Name string `json:"name"` // 项目名称
|
||||||
GameOnlineID uint16 `json:"port_bl"` //这个是命令行输入的参数
|
// LoginPort string `json:"port"`
|
||||||
ServerInfo ServerList
|
GameOnlineID uint32 `json:"port_bl"` //这个是命令行输入的参数
|
||||||
|
ServerInfo ServerList
|
||||||
Address string //rpc端口
|
|
||||||
|
Address string //rpc端口
|
||||||
}
|
|
||||||
type ServerList struct {
|
}
|
||||||
OnlineID uint16 `gorm:"column:online_id;comment:'在线ID';uniqueIndex" json:"online_id"`
|
type ServerList struct {
|
||||||
//服务器名称Desc
|
OnlineID uint32 `gorm:"column:online_id;comment:'在线ID';uniqueIndex" json:"online_id"`
|
||||||
Name string `gorm:"comment:'服务器名称'" json:"name"`
|
//服务器名称Desc
|
||||||
IP string `gorm:"type:string;comment:'服务器IP'" json:"ip"`
|
Name string `gorm:"comment:'服务器名称'" json:"name"`
|
||||||
Port uint16 `gorm:"comment:'端口号,通常是小整数'" json:"port"`
|
IP string `gorm:"type:string;comment:'服务器IP'" json:"ip"`
|
||||||
IsOpen uint8 `gorm:"default:0;not null;comment:'是否开启'" json:"is_open"`
|
Port uint32 `gorm:"comment:'端口号,通常是小整数'" json:"port"`
|
||||||
//登录地址
|
IsOpen uint8 `gorm:"default:0;not null;comment:'是否开启'" json:"is_open"`
|
||||||
LoginAddr string `gorm:"type:string;comment:'登录地址'" json:"login_addr"`
|
//登录地址
|
||||||
//账号
|
LoginAddr string `gorm:"type:string;comment:'登录地址'" json:"login_addr"`
|
||||||
Account string `gorm:"type:string;comment:'账号'" json:"account"`
|
//账号
|
||||||
//密码
|
Account string `gorm:"type:string;comment:'账号'" json:"account"`
|
||||||
Password string `gorm:"type:string;comment:'密码'" json:"password"`
|
//密码
|
||||||
CanPort []uint32 `gorm:"type:jsonb;comment:'可连接端口'" json:"can_port"`
|
Password string `gorm:"type:string;comment:'密码'" json:"password"`
|
||||||
//是否测试服
|
CanPort []uint32 `gorm:"type:jsonb;comment:'可连接端口'" json:"can_port"`
|
||||||
IsVip uint32 `gorm:"default:0;not null;comment:'是否为VIP服务器'" json:"is_vip"`
|
//是否测试服
|
||||||
//isdebug 是否本地服
|
IsVip uint32 `gorm:"default:0;not null;comment:'是否为VIP服务器'" json:"is_vip"`
|
||||||
IsDebug uint8 `gorm:"default:0;comment:'是否为调试模式'" json:"is_debug"`
|
//isdebug 是否本地服
|
||||||
//服务器异色概率设定ServerList
|
IsDebug uint8 `gorm:"default:0;comment:'是否为调试模式'" json:"is_debug"`
|
||||||
ShinyRate uint8 `gorm:"default:0;comment:'异色概率'" json:"shiny_rate"`
|
|
||||||
//服务器天气设定ServerList
|
//服务器属主Desc
|
||||||
WeatherRate uint8 `gorm:"default:0;comment:'天气概率'" json:"weather_rate"`
|
Owner uint32 `gorm:"comment:'服务器属主'" json:"owner"`
|
||||||
|
Desc string `gorm:"comment:'服务器描述'" json:"desc"`
|
||||||
//服务器属主Desc
|
OldScreen string `gorm:"comment:'服务器screen参数'" json:"old_screen"`
|
||||||
Owner uint32 `gorm:"comment:'服务器属主'" json:"owner"`
|
//到期时间ServerList
|
||||||
Desc string `gorm:"comment:'服务器描述'" json:"desc"`
|
ExpireTime time.Time `gorm:"default:0;comment:'到期时间'" json:"expire_time"`
|
||||||
OldScreen string `gorm:"comment:'服务器screen参数'" json:"old_screen"`
|
}
|
||||||
//到期时间ServerList
|
|
||||||
ExpireTime time.Time `gorm:"default:0;comment:'到期时间'" json:"expire_time"`
|
func (s *ServerList) GetID() string {
|
||||||
}
|
return gconv.String(100000*s.OnlineID + s.Port)
|
||||||
|
}
|
||||||
// OSS相关配置
|
|
||||||
type oss struct {
|
// OSS相关配置
|
||||||
Endpoint string `json:"endpoint"`
|
type oss struct {
|
||||||
AccessKeyID string `json:"accessKeyID"`
|
Endpoint string `json:"endpoint"`
|
||||||
SecretAccessKey string `json:"secretAccessKey"`
|
AccessKeyID string `json:"accessKeyID"`
|
||||||
UseSSL bool `json:"useSSL"`
|
SecretAccessKey string `json:"secretAccessKey"`
|
||||||
BucketName string `json:"bucketName"`
|
UseSSL bool `json:"useSSL"`
|
||||||
Location string `json:"location"`
|
BucketName string `json:"bucketName"`
|
||||||
}
|
Location string `json:"location"`
|
||||||
|
}
|
||||||
// 文件上传配置
|
|
||||||
type file struct {
|
// 文件上传配置
|
||||||
Mode string `json:"mode"` // 模式 local oss
|
type file struct {
|
||||||
Domain string `json:"domain"` // 域名 http://
|
Mode string `json:"mode"` // 模式 local oss
|
||||||
Oss *oss `json:"oss,omitempty"`
|
Domain string `json:"domain"` // 域名 http://
|
||||||
}
|
Oss *oss `json:"oss,omitempty"`
|
||||||
|
}
|
||||||
// NewConfig new config
|
|
||||||
func newConfig() *sConfig {
|
// NewConfig new config
|
||||||
var ctx g.Ctx
|
func newConfig() *sConfig {
|
||||||
config := &sConfig{
|
var ctx g.Ctx
|
||||||
AutoMigrate: GetCfgWithDefault(ctx, "blazing.autoMigrate", g.NewVar(false)).Bool(),
|
config := &sConfig{
|
||||||
Name: GetCfgWithDefault(ctx, "server.name", g.NewVar("")).String(),
|
AutoMigrate: GetCfgWithDefault(ctx, "blazing.autoMigrate", g.NewVar(false)).Bool(),
|
||||||
|
Name: GetCfgWithDefault(ctx, "server.name", g.NewVar("")).String(),
|
||||||
Eps: GetCfgWithDefault(ctx, "blazing.eps", g.NewVar(false)).Bool(),
|
|
||||||
// LoginPort: string(GetCfgWithDefault(ctx, "server.port", g.NewVar("8080")).String()),
|
Eps: GetCfgWithDefault(ctx, "blazing.eps", g.NewVar(false)).Bool(),
|
||||||
Address: GetCfgWithDefault(ctx, "server.address", g.NewVar("8080")).String(),
|
// LoginPort: string(GetCfgWithDefault(ctx, "server.port", g.NewVar("8080")).String()),
|
||||||
//GamePort: GetCfgWithDefault(ctx, "server.game", g.NewVar("8080")).Uint64s(),
|
Address: GetCfgWithDefault(ctx, "server.address", g.NewVar("8080")).String(),
|
||||||
|
//GamePort: GetCfgWithDefault(ctx, "server.game", g.NewVar("8080")).Uint64s(),
|
||||||
File: &file{
|
|
||||||
Mode: GetCfgWithDefault(ctx, "blazing.file.mode", g.NewVar("none")).String(),
|
File: &file{
|
||||||
Domain: GetCfgWithDefault(ctx, "blazing.file.domain", g.NewVar("http://127.0.0.1:8300")).String(),
|
Mode: GetCfgWithDefault(ctx, "blazing.file.mode", g.NewVar("none")).String(),
|
||||||
Oss: &oss{
|
Domain: GetCfgWithDefault(ctx, "blazing.file.domain", g.NewVar("http://127.0.0.1:8300")).String(),
|
||||||
Endpoint: GetCfgWithDefault(ctx, "blazing.file.oss.endpoint", g.NewVar("127.0.0.1:9000")).String(),
|
Oss: &oss{
|
||||||
AccessKeyID: GetCfgWithDefault(ctx, "blazing.file.oss.accessKeyID", g.NewVar("")).String(),
|
Endpoint: GetCfgWithDefault(ctx, "blazing.file.oss.endpoint", g.NewVar("127.0.0.1:9000")).String(),
|
||||||
SecretAccessKey: GetCfgWithDefault(ctx, "blazing.file.oss.secretAccessKey", g.NewVar("")).String(),
|
AccessKeyID: GetCfgWithDefault(ctx, "blazing.file.oss.accessKeyID", g.NewVar("")).String(),
|
||||||
UseSSL: GetCfgWithDefault(ctx, "blazing.file.oss.useSSL", g.NewVar(false)).Bool(),
|
SecretAccessKey: GetCfgWithDefault(ctx, "blazing.file.oss.secretAccessKey", g.NewVar("")).String(),
|
||||||
BucketName: GetCfgWithDefault(ctx, "blazing.file.oss.bucketName", g.NewVar("blazing")).String(),
|
UseSSL: GetCfgWithDefault(ctx, "blazing.file.oss.useSSL", g.NewVar(false)).Bool(),
|
||||||
Location: GetCfgWithDefault(ctx, "blazing.file.oss.location", g.NewVar("us-east-1")).String(),
|
BucketName: GetCfgWithDefault(ctx, "blazing.file.oss.bucketName", g.NewVar("blazing")).String(),
|
||||||
},
|
Location: GetCfgWithDefault(ctx, "blazing.file.oss.location", g.NewVar("us-east-1")).String(),
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
}
|
||||||
return config
|
|
||||||
}
|
return config
|
||||||
|
}
|
||||||
// qiniu 七牛云配置
|
|
||||||
type qiniu struct {
|
// qiniu 七牛云配置
|
||||||
AccessKey string `json:"ak"`
|
type qiniu struct {
|
||||||
SecretKey string `json:"sk"`
|
AccessKey string `json:"ak"`
|
||||||
Bucket string `json:"bucket"`
|
SecretKey string `json:"sk"`
|
||||||
CDN string `json:"cdn"`
|
Bucket string `json:"bucket"`
|
||||||
}
|
CDN string `json:"cdn"`
|
||||||
|
}
|
||||||
// Config config
|
|
||||||
var Config = newConfig()
|
// Config config
|
||||||
|
var Config = newConfig()
|
||||||
// GetCfgWithDefault get config with default value
|
|
||||||
func GetCfgWithDefault(ctx g.Ctx, key string, defaultValue *g.Var) *g.Var {
|
// GetCfgWithDefault get config with default value
|
||||||
value, err := g.Cfg().GetWithEnv(ctx, key)
|
func GetCfgWithDefault(ctx g.Ctx, key string, defaultValue *g.Var) *g.Var {
|
||||||
if err != nil {
|
value, err := g.Cfg().GetWithEnv(ctx, key)
|
||||||
return defaultValue
|
if err != nil {
|
||||||
}
|
return defaultValue
|
||||||
if value.IsEmpty() || value.IsNil() {
|
}
|
||||||
return defaultValue
|
if value.IsEmpty() || value.IsNil() {
|
||||||
}
|
return defaultValue
|
||||||
return value
|
}
|
||||||
}
|
return value
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func RunFunc(ctx g.Ctx, funcstring string) (err error) {
|
|||||||
// ClusterRunFunc 集群运行函数,如果是单机模式, 则直接运行函数
|
// ClusterRunFunc 集群运行函数,如果是单机模式, 则直接运行函数
|
||||||
func ClusterRunFunc(ctx g.Ctx, funcstring string) (err error) {
|
func ClusterRunFunc(ctx g.Ctx, funcstring string) (err error) {
|
||||||
if IsRedisMode {
|
if IsRedisMode {
|
||||||
conn, err := g.Redis("cool").Conn(ctx)
|
conn, err := Redis.Conn(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -63,41 +63,3 @@ func ClusterRunFunc(ctx g.Ctx, funcstring string) (err error) {
|
|||||||
return RunFunc(ctx, funcstring)
|
return RunFunc(ctx, funcstring)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenFunc 监听函数
|
|
||||||
func ListenFunc(ctx g.Ctx) {
|
|
||||||
if IsRedisMode {
|
|
||||||
conn, err := g.Redis("cool").Conn(ctx)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer conn.Close(ctx)
|
|
||||||
_, err = conn.Do(ctx, "subscribe", "cool:func")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
data, err := conn.Receive(ctx)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Error(ctx, err)
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if data != nil {
|
|
||||||
dataMap := data.MapStrStr()
|
|
||||||
if dataMap["Kind"] == "subscribe" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if dataMap["Channel"] == "cool:func" {
|
|
||||||
Logger.Debug(ctx, "执行函数", dataMap["Payload"])
|
|
||||||
err := RunFunc(ctx, dataMap["Payload"])
|
|
||||||
if err != nil {
|
|
||||||
Logger.Error(ctx, "执行函数失败", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,14 +22,22 @@ var ctx = context.TODO()
|
|||||||
type Cmd struct {
|
type Cmd struct {
|
||||||
Func reflect.Value //方法函数
|
Func reflect.Value //方法函数
|
||||||
Req reflect.Type //请求体
|
Req reflect.Type //请求体
|
||||||
|
// HeaderFieldIndex 是请求结构体中 TomeeHeader 字段的索引路径。
|
||||||
|
HeaderFieldIndex []int
|
||||||
|
// UseConn 标记第二个参数是否为 gnet.Conn。
|
||||||
|
UseConn bool
|
||||||
|
// 新增:预缓存的req创建函数(返回结构体指针)
|
||||||
|
NewReqFunc func() interface{}
|
||||||
|
// NewReqValue 返回请求结构体指针的 reflect.Value,避免重复构造类型信息。
|
||||||
|
NewReqValue func() reflect.Value
|
||||||
//Res reflect.Value //返回体
|
//Res reflect.Value //返回体
|
||||||
}
|
}
|
||||||
|
|
||||||
var CmdCache = make(map[uint32]Cmd, 0)
|
var CmdCache = make(map[uint32]Cmd, 0)
|
||||||
var (
|
var (
|
||||||
Logger = glog.New()
|
Logger = glog.New()
|
||||||
Cron = cronex.New() //时间轮
|
Cron = cronex.New() //时间轮
|
||||||
|
Connected int64
|
||||||
)
|
)
|
||||||
var Filter *sensitive.Manager
|
var Filter *sensitive.Manager
|
||||||
var DefaultGenerator = utils.NewGen(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), uint8(Config.GameOnlineID))
|
var DefaultGenerator = utils.NewGen(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), uint8(Config.GameOnlineID))
|
||||||
@@ -37,7 +45,7 @@ var DefaultGenerator = utils.NewGen(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
|||||||
func init() {
|
func init() {
|
||||||
// 创建 IdGeneratorOptions 对象,可在构造函数中输入 WorkerId:
|
// 创建 IdGeneratorOptions 对象,可在构造函数中输入 WorkerId:
|
||||||
Logger.SetFlags(glog.F_TIME_STD | glog.F_FILE_LONG | glog.F_ASYNC) //设置flag
|
Logger.SetFlags(glog.F_TIME_STD | glog.F_FILE_LONG | glog.F_ASYNC) //设置flag
|
||||||
Logger.Print(ctx, "初始化日志")
|
|
||||||
// for i := 0; i < 600; i++ {
|
// for i := 0; i < 600; i++ {
|
||||||
// glog.Debug(context.Background(), i, "初始化雪花算法", DefaultGenerator.Get())
|
// glog.Debug(context.Background(), i, "初始化雪花算法", DefaultGenerator.Get())
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ require (
|
|||||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
|
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/imroc/req/v3 v3.43.3 // indirect
|
|
||||||
github.com/klauspost/compress v1.17.7 // indirect
|
github.com/klauspost/compress v1.17.7 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.16.0 // indirect
|
github.com/onsi/ginkgo/v2 v2.16.0 // indirect
|
||||||
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
|
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package cool
|
|||||||
import (
|
import (
|
||||||
_ "blazing/contrib/drivers/pgsql"
|
_ "blazing/contrib/drivers/pgsql"
|
||||||
"blazing/cool/cooldb"
|
"blazing/cool/cooldb"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/encoding/gjson"
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
@@ -10,6 +11,11 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
autoMigrateMu sync.Mutex
|
||||||
|
autoMigrateModels []IModel
|
||||||
|
)
|
||||||
|
|
||||||
// 初始化数据库连接供gorm使用
|
// 初始化数据库连接供gorm使用
|
||||||
func InitDB(group string) (*gorm.DB, error) {
|
func InitDB(group string) (*gorm.DB, error) {
|
||||||
// var ctx context.Context
|
// var ctx context.Context
|
||||||
@@ -54,9 +60,33 @@ func getDBbyModel(model IModel) *gorm.DB {
|
|||||||
|
|
||||||
// 根据entity结构体创建表
|
// 根据entity结构体创建表
|
||||||
func CreateTable(model IModel) error {
|
func CreateTable(model IModel) error {
|
||||||
if Config.AutoMigrate {
|
autoMigrateMu.Lock()
|
||||||
|
autoMigrateModels = append(autoMigrateModels, model)
|
||||||
|
autoMigrateMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunAutoMigrate 显式执行已注册模型的建表/迁移。
|
||||||
|
func RunAutoMigrate() error {
|
||||||
|
if !Config.AutoMigrate {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
autoMigrateMu.Lock()
|
||||||
|
models := append([]IModel(nil), autoMigrateModels...)
|
||||||
|
autoMigrateMu.Unlock()
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(models))
|
||||||
|
for _, model := range models {
|
||||||
|
key := model.GroupName() + ":" + model.TableName()
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
|
||||||
db := getDBbyModel(model)
|
db := getDBbyModel(model)
|
||||||
return db.AutoMigrate(model)
|
if err := db.AutoMigrate(model); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,35 @@
|
|||||||
package cool
|
package cool
|
||||||
|
|
||||||
// 存值示例
|
// 存值示例
|
||||||
func AddClient(id uint16, client *ClientHandler) {
|
func AddClient(id uint32, client *ClientHandler) {
|
||||||
// 普通map:Clientmap[id] = client
|
// 普通map:Clientmap[id] = client
|
||||||
Clientmap.Store(id, client) // sync.Map存值
|
Clientmap.Store(id, client) // sync.Map存值
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理指定client(uid=100000*onlineID+port)
|
||||||
|
func DeleteClientOnly(uid uint32) {
|
||||||
|
Clientmap.Delete(uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理指定client(onlineID+port)
|
||||||
|
func DeleteClient(id, port uint32) {
|
||||||
|
Clientmap.Delete(100000*id + port)
|
||||||
|
}
|
||||||
|
|
||||||
// 取值示例
|
// 取值示例
|
||||||
func GetClient(id uint16) (*ClientHandler, bool) {
|
func GetClient(id, port uint32) (*ClientHandler, bool) {
|
||||||
// 普通map:client, ok := Clientmap[id]
|
// 普通map:client, ok := Clientmap[id]
|
||||||
val, ok := Clientmap.Load(id) // sync.Map取值
|
val, ok := Clientmap.Load(100000*id + port) // sync.Map取值
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
// 类型断言(确保value是*ClientHandler)
|
||||||
|
client, ok := val.(*ClientHandler)
|
||||||
|
return client, ok
|
||||||
|
}
|
||||||
|
func GetClientOnly(uid uint32) (*ClientHandler, bool) {
|
||||||
|
// 普通map:client, ok := Clientmap[id]
|
||||||
|
val, ok := Clientmap.Load(uid) // sync.Map取值
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cool
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/container/garray"
|
"github.com/gogf/gf/v2/container/garray"
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
@@ -11,20 +12,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type IService interface {
|
type IService interface {
|
||||||
ServiceAdd(ctx context.Context, req *AddReq) (data interface{}, err error) // 新增
|
ServiceAdd(ctx context.Context, req *AddReq) (data interface{}, err error) // 新增
|
||||||
ServiceDelete(ctx context.Context, req *DeleteReq) (data interface{}, err error) // 删除
|
ServiceDelete(ctx context.Context, req *DeleteReq) (data sql.Result, err error) // 删除
|
||||||
ServiceUpdate(ctx context.Context, req *UpdateReq) (data interface{}, err error) // 修改
|
ServiceUpdate(ctx context.Context, req *UpdateReq) (data sql.Result, err error) // 修改
|
||||||
ServiceInfo(ctx context.Context, req *InfoReq) (data interface{}, err error) // 详情
|
ServiceInfo(ctx context.Context, req *InfoReq) (data interface{}, err error) // 详情
|
||||||
ServiceList(ctx context.Context, req *ListReq) (data interface{}, err error) // 列表
|
ServiceList(ctx context.Context, req *ListReq) (data interface{}, err error) // 列表
|
||||||
ServicePage(ctx context.Context, req *PageReq) (data interface{}, err error) // 分页
|
ServicePage(ctx context.Context, req *PageReq) (data interface{}, err error) // 分页
|
||||||
ModifyBefore(ctx context.Context, method string, param g.MapStrAny) (err error) // 新增|删除|修改前的操作
|
ModifyBefore(ctx context.Context, method string, param g.MapStrAny) (err error) // 新增|删除|修改前的操作
|
||||||
ModifyAfter(ctx context.Context, method string, param g.MapStrAny) (err error) // 新增|删除|修改后的操作
|
ModifyAfter(ctx context.Context, method string, param g.MapStrAny) (err error) // 新增|删除|修改后的操作
|
||||||
GetModel() IModel // 获取model
|
GetModel() IModel // 获取model
|
||||||
}
|
}
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Model IModel
|
Model IModel
|
||||||
ListQueryOp *QueryOp
|
ListQueryOp *QueryOp
|
||||||
PageQueryOp *QueryOp
|
PageQueryOp *QueryOp
|
||||||
|
Where func(ctx context.Context) []g.Array // 删除修改定义条件
|
||||||
InsertParam func(ctx context.Context) g.MapStrAny // Add时插入参数
|
InsertParam func(ctx context.Context) g.MapStrAny // Add时插入参数
|
||||||
Before func(ctx context.Context) (err error) // CRUD前的操作
|
Before func(ctx context.Context) (err error) // CRUD前的操作
|
||||||
InfoIgnoreProperty string // Info时忽略的字段,多个字段用逗号隔开
|
InfoIgnoreProperty string // Info时忽略的字段,多个字段用逗号隔开
|
||||||
@@ -35,11 +37,13 @@ type Service struct {
|
|||||||
|
|
||||||
// List/Add接口条件配置
|
// List/Add接口条件配置
|
||||||
type QueryOp struct {
|
type QueryOp struct {
|
||||||
FieldEQ []string // 字段等于 多个字段选择以及高级搜索都是这个
|
FieldEQ []string // 字段等于 多个字段选择以及高级搜索都是这个
|
||||||
KeyWordField []string // 模糊搜索匹配的数据库字段,对应普通搜索
|
DataFieldEQ []string // 新增:JSONB data->>'xxx' 字段 = ? 多个字段选择以及高级搜索都是这个
|
||||||
AddOrderby g.MapStrStr // 添加排序
|
KeyWordField []string // 模糊搜索匹配的数据库字段,对应普通搜索
|
||||||
Where func(ctx context.Context) []g.Array // 自定义条件
|
AddOrderby g.MapStrStr // 添加排序
|
||||||
Select string // 查询字段,多个字段用逗号隔开 如: id,name 或 a.id,a.name,b.name AS bname
|
Where func(ctx context.Context) []g.Array // 自定义条件
|
||||||
|
Select string // 查询字段,多个字段用逗号隔开 如: id,name 或 a.id,a.name,b.name AS bname
|
||||||
|
|
||||||
Join []*JoinOp // 关联查询
|
Join []*JoinOp // 关联查询
|
||||||
Extend func(ctx g.Ctx, m *gdb.Model) *gdb.Model // 追加其他条件
|
Extend func(ctx g.Ctx, m *gdb.Model) *gdb.Model // 追加其他条件
|
||||||
ModifyResult func(ctx g.Ctx, data interface{}) interface{} // 修改结果
|
ModifyResult func(ctx g.Ctx, data interface{}) interface{} // 修改结果
|
||||||
@@ -104,16 +108,31 @@ func (s *Service) ServiceAdd(ctx context.Context, req *AddReq) (data interface{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServiceDelete 删除
|
// ServiceDelete 删除
|
||||||
func (s *Service) ServiceDelete(ctx context.Context, req *DeleteReq) (data interface{}, err error) {
|
func (s *Service) ServiceDelete(ctx context.Context, req *DeleteReq) (data sql.Result, err error) {
|
||||||
ids := g.RequestFromCtx(ctx).Get("ids").Slice()
|
ids := g.RequestFromCtx(ctx).Get("ids").Slice()
|
||||||
m := g.DB(s.Model.GroupName()).Model(s.Model.TableName())
|
m := g.DB(s.Model.GroupName()).Model(s.Model.TableName())
|
||||||
|
if s.Where != nil {
|
||||||
|
where := s.Where(ctx)
|
||||||
|
if len(where) > 0 {
|
||||||
|
for _, v := range where {
|
||||||
|
if len(v) == 3 {
|
||||||
|
if gconv.Bool(v[2]) {
|
||||||
|
m.Where(v[0], v[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(v) == 2 {
|
||||||
|
m.Where(v[0], v[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
data, err = m.WhereIn("id", ids).Delete()
|
data, err = m.WhereIn("id", ids).Delete()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceUpdate 修改
|
// ServiceUpdate 修改
|
||||||
func (s *Service) ServiceUpdate(ctx context.Context, req *UpdateReq) (data interface{}, err error) {
|
func (s *Service) ServiceUpdate(ctx context.Context, req *UpdateReq) (data sql.Result, err error) {
|
||||||
r := g.RequestFromCtx(ctx)
|
r := g.RequestFromCtx(ctx)
|
||||||
rmap := r.GetMap()
|
rmap := r.GetMap()
|
||||||
if rmap["id"] == nil {
|
if rmap["id"] == nil {
|
||||||
@@ -135,7 +154,24 @@ func (s *Service) ServiceUpdate(ctx context.Context, req *UpdateReq) (data inter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m := DBM(s.Model)
|
m := DBM(s.Model)
|
||||||
_, err = m.Data(rmap).Where("id", rmap["id"]).Update()
|
rmap["updateTime"] = nil
|
||||||
|
if s.Where != nil {
|
||||||
|
where := s.Where(ctx)
|
||||||
|
if len(where) > 0 {
|
||||||
|
for _, v := range where {
|
||||||
|
if len(v) == 3 {
|
||||||
|
if gconv.Bool(v[2]) {
|
||||||
|
m.Where(v[0], v[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(v) == 2 {
|
||||||
|
m.Where(v[0], v[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//rmap["id"] = nil
|
||||||
|
data, err = m.Data(rmap).Where("id", rmap["id"]).Update()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -199,6 +235,18 @@ func (s *Service) ServiceList(ctx context.Context, req *ListReq) (data interface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. JSONB data->>'xxx' 字段查询(你要的 data 类查找)
|
||||||
|
if len(s.ListQueryOp.DataFieldEQ) > 0 {
|
||||||
|
for _, field := range s.ListQueryOp.DataFieldEQ {
|
||||||
|
if val := r.Get(field); val.String() != "" {
|
||||||
|
// 关键:拼接 data->>'字段名' = ?
|
||||||
|
// 错误写法:m.Where("data->>::TEXT? = ?", field, val)
|
||||||
|
// 正确写法:
|
||||||
|
m.Where("(data->>?)::TEXT = ?", field, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// 如果KeyWordField不为空 则添加查询条件
|
// 如果KeyWordField不为空 则添加查询条件
|
||||||
if !r.Get("keyWord").IsEmpty() {
|
if !r.Get("keyWord").IsEmpty() {
|
||||||
if len(s.ListQueryOp.KeyWordField) > 0 {
|
if len(s.ListQueryOp.KeyWordField) > 0 {
|
||||||
@@ -206,7 +254,9 @@ func (s *Service) ServiceList(ctx context.Context, req *ListReq) (data interface
|
|||||||
for _, field := range s.ListQueryOp.KeyWordField {
|
for _, field := range s.ListQueryOp.KeyWordField {
|
||||||
// g.DumpWithType(field)
|
// g.DumpWithType(field)
|
||||||
// builder.WhereLike(field, "%"+r.Get("keyWord").String()+"%")
|
// builder.WhereLike(field, "%"+r.Get("keyWord").String()+"%")
|
||||||
builder = builder.WhereOrLike(field, "%"+r.Get("keyWord").String()+"%")
|
|
||||||
|
builder = builder.WhereOrf(field+"::text LIKE ?", "%"+r.Get("keyWord").String()+"%")
|
||||||
|
//builder = builder.WhereOrLike(field, "%"+r.Get("keyWord").String()+"%")
|
||||||
}
|
}
|
||||||
m.Where(builder)
|
m.Where(builder)
|
||||||
}
|
}
|
||||||
@@ -218,6 +268,8 @@ func (s *Service) ServiceList(ctx context.Context, req *ListReq) (data interface
|
|||||||
if len(v) == 3 {
|
if len(v) == 3 {
|
||||||
if gconv.Bool(v[2]) {
|
if gconv.Bool(v[2]) {
|
||||||
m.Where(v[0], v[1])
|
m.Where(v[0], v[1])
|
||||||
|
} else {
|
||||||
|
m.WhereNot(gconv.String(v[0]), v[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(v) == 2 {
|
if len(v) == 2 {
|
||||||
@@ -301,6 +353,18 @@ func (s *Service) ServicePage(ctx context.Context, req *PageReq) (data interface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. JSONB data->>'xxx' 字段查询(你要的 data 类查找)
|
||||||
|
if len(s.PageQueryOp.DataFieldEQ) > 0 {
|
||||||
|
for _, field := range s.PageQueryOp.DataFieldEQ {
|
||||||
|
if val := r.Get(field); val.String() != "" {
|
||||||
|
// 关键:拼接 data->>'字段名' = ?
|
||||||
|
// 错误写法:m.Where("data->>::TEXT? = ?", field, val)
|
||||||
|
// 正确写法:
|
||||||
|
m.Where("(data->>?)::TEXT = ?", field, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// 如果KeyWordField不为空 则添加查询条件
|
// 如果KeyWordField不为空 则添加查询条件
|
||||||
if !r.Get("keyWord").IsEmpty() {
|
if !r.Get("keyWord").IsEmpty() {
|
||||||
if len(s.PageQueryOp.KeyWordField) > 0 {
|
if len(s.PageQueryOp.KeyWordField) > 0 {
|
||||||
@@ -321,6 +385,8 @@ func (s *Service) ServicePage(ctx context.Context, req *PageReq) (data interface
|
|||||||
if len(v) == 3 {
|
if len(v) == 3 {
|
||||||
if gconv.Bool(v[2]) {
|
if gconv.Bool(v[2]) {
|
||||||
m.Where(v[0], v[1])
|
m.Where(v[0], v[1])
|
||||||
|
} else {
|
||||||
|
m.WhereNot(gconv.String(v[0]), v[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(v) == 2 {
|
if len(v) == 2 {
|
||||||
|
|||||||
@@ -42,15 +42,15 @@ const (
|
|||||||
maxMatrixSize = 227 // 矩阵维度(覆盖最大属性ID 226)
|
maxMatrixSize = 227 // 矩阵维度(覆盖最大属性ID 226)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 合法单属性ID集合(快速校验)
|
// 合法单属性ID集合(按ID直接索引,避免运行时 map 查找)
|
||||||
var validSingleElementIDs = map[int]bool{
|
var validSingleElementIDs = [maxMatrixSize]bool{
|
||||||
1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true,
|
1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true,
|
||||||
11: true, 12: true, 13: true, 14: true, 15: true, 16: true, 17: true, 18: true, 19: true, 20: true,
|
11: true, 12: true, 13: true, 14: true, 15: true, 16: true, 17: true, 18: true, 19: true, 20: true,
|
||||||
221: true, 222: true, 223: true, 224: true, 225: true, 226: true,
|
221: true, 222: true, 223: true, 224: true, 225: true, 226: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 元素名称映射(全属性对应,便于日志输出)
|
// 元素名称映射(按ID直接索引,便于日志输出)
|
||||||
var elementNameMap = map[ElementType]string{
|
var elementNameMap = [maxMatrixSize]string{
|
||||||
ElementTypeGrass: "GRASS",
|
ElementTypeGrass: "GRASS",
|
||||||
ElementTypeWater: "WATER",
|
ElementTypeWater: "WATER",
|
||||||
ElementTypeFire: "FIRE",
|
ElementTypeFire: "FIRE",
|
||||||
@@ -198,46 +198,55 @@ type ElementCombination struct {
|
|||||||
ID int // 组合唯一ID
|
ID int // 组合唯一ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局预加载资源(程序启动时init初始化,运行时直接使用)
|
// 全局预加载资源(程序启动时初始化,运行时只读)
|
||||||
var (
|
var (
|
||||||
// 元素组合池:key=组合ID,value=组合实例(预加载所有合法组合)
|
validCombinationIDs [maxMatrixSize]bool
|
||||||
elementCombinationPool = make(map[int]*ElementCombination, 150) // 128双+26单=154,预分配足够容量
|
elementCombinationPool [maxMatrixSize]ElementCombination
|
||||||
// 单属性克制矩阵(预初始化所有特殊克制关系,默认1.0)
|
dualElementSecondaryPool [maxMatrixSize]ElementType
|
||||||
matrix [maxMatrixSize][maxMatrixSize]float64
|
matrix [maxMatrixSize][maxMatrixSize]float64
|
||||||
|
Calculator *ElementCalculator
|
||||||
)
|
)
|
||||||
|
|
||||||
// init 预加载所有资源(程序启动时执行一次,无并发问题)
|
// init 预加载所有资源(程序启动时执行一次,无并发问题)
|
||||||
func init() {
|
func init() {
|
||||||
// 1. 初始化单属性克制矩阵
|
|
||||||
initFullTableMatrix()
|
initFullTableMatrix()
|
||||||
|
initElementCombinationPool()
|
||||||
|
Calculator = NewElementCalculator()
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 预加载所有单属性组合
|
func initElementCombinationPool() {
|
||||||
for id := range validSingleElementIDs {
|
for id, valid := range validSingleElementIDs {
|
||||||
combo := &ElementCombination{
|
if !valid {
|
||||||
Primary: ElementType(id),
|
continue
|
||||||
Secondary: nil,
|
}
|
||||||
ID: id,
|
validCombinationIDs[id] = true
|
||||||
|
elementCombinationPool[id] = ElementCombination{
|
||||||
|
Primary: ElementType(id),
|
||||||
|
ID: id,
|
||||||
}
|
}
|
||||||
elementCombinationPool[id] = combo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 预加载所有双属性组合
|
|
||||||
for dualID, atts := range dualElementMap {
|
for dualID, atts := range dualElementMap {
|
||||||
primaryID, secondaryID := atts[0], atts[1]
|
primaryID, secondaryID := atts[0], atts[1]
|
||||||
// 按ID升序排序,保证组合一致性
|
|
||||||
primary, secondary := ElementType(primaryID), ElementType(secondaryID)
|
primary, secondary := ElementType(primaryID), ElementType(secondaryID)
|
||||||
if primary > secondary {
|
if primary > secondary {
|
||||||
primary, secondary = secondary, primary
|
primary, secondary = secondary, primary
|
||||||
}
|
}
|
||||||
combo := &ElementCombination{
|
|
||||||
|
dualElementSecondaryPool[dualID] = secondary
|
||||||
|
validCombinationIDs[dualID] = true
|
||||||
|
elementCombinationPool[dualID] = ElementCombination{
|
||||||
Primary: primary,
|
Primary: primary,
|
||||||
Secondary: &secondary,
|
Secondary: &dualElementSecondaryPool[dualID],
|
||||||
ID: dualID,
|
ID: dualID,
|
||||||
}
|
}
|
||||||
elementCombinationPool[dualID] = combo
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isValidCombinationID(id int) bool {
|
||||||
|
return id > 0 && id < maxMatrixSize && validCombinationIDs[id]
|
||||||
|
}
|
||||||
|
|
||||||
// IsDual 判断是否为双属性
|
// IsDual 判断是否为双属性
|
||||||
func (ec *ElementCombination) IsDual() bool {
|
func (ec *ElementCombination) IsDual() bool {
|
||||||
return ec.Secondary != nil
|
return ec.Secondary != nil
|
||||||
@@ -245,84 +254,82 @@ func (ec *ElementCombination) IsDual() bool {
|
|||||||
|
|
||||||
// Elements 获取所有属性列表
|
// Elements 获取所有属性列表
|
||||||
func (ec *ElementCombination) Elements() []ElementType {
|
func (ec *ElementCombination) Elements() []ElementType {
|
||||||
if ec.IsDual() {
|
if secondary := ec.Secondary; secondary != nil {
|
||||||
return []ElementType{ec.Primary, *ec.Secondary}
|
return []ElementType{ec.Primary, *secondary}
|
||||||
}
|
}
|
||||||
return []ElementType{ec.Primary}
|
return []ElementType{ec.Primary}
|
||||||
}
|
}
|
||||||
|
|
||||||
// String 友好格式化输出
|
// String 友好格式化输出
|
||||||
func (ec *ElementCombination) String() string {
|
func (ec *ElementCombination) String() string {
|
||||||
primaryName := elementNameMap[ec.Primary]
|
if secondary := ec.Secondary; secondary != nil {
|
||||||
if !ec.IsDual() {
|
return fmt.Sprintf("(%s, %s)", elementNameMap[ec.Primary], elementNameMap[*secondary])
|
||||||
return fmt.Sprintf("(%s)", primaryName)
|
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("(%s, %s)", primaryName, elementNameMap[*ec.Secondary])
|
return fmt.Sprintf("(%s)", elementNameMap[ec.Primary])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ElementCalculator 无锁元素克制计算器(依赖预加载资源)
|
// ElementCalculator 无锁元素克制计算器(所有倍数在初始化阶段预计算)
|
||||||
type ElementCalculator struct {
|
type ElementCalculator struct {
|
||||||
offensiveCache map[string]float64 // 攻击克制缓存(运行时填充,无并发写)
|
offensiveTable [maxMatrixSize][maxMatrixSize]float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewElementCalculator 创建计算器实例(仅初始化缓存)
|
// NewElementCalculator 创建计算器实例(构建只读查表缓存)
|
||||||
func NewElementCalculator() *ElementCalculator {
|
func NewElementCalculator() *ElementCalculator {
|
||||||
return &ElementCalculator{
|
c := &ElementCalculator{}
|
||||||
offensiveCache: make(map[string]float64, 4096), // 预分配大容量缓存
|
c.initOffensiveTable()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ElementCalculator) initOffensiveTable() {
|
||||||
|
for attackerID, valid := range validCombinationIDs {
|
||||||
|
if !valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
attacker := &elementCombinationPool[attackerID]
|
||||||
|
for defenderID, valid := range validCombinationIDs {
|
||||||
|
if !valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defender := &elementCombinationPool[defenderID]
|
||||||
|
c.offensiveTable[attackerID][defenderID] = c.calculateMultiplier(attacker, defender)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMatrixValue 直接返回矩阵值(修复核心问题:不再将0转换为1)
|
// getMatrixValue 直接返回矩阵值(修复核心问题:不再将0转换为1)
|
||||||
func (c *ElementCalculator) getMatrixValue(attacker, defender ElementType) float64 {
|
func (c *ElementCalculator) getMatrixValue(attacker, defender ElementType) float64 {
|
||||||
return matrix[attacker][defender] // 矩阵默认已初始化1.0,特殊值直接返回
|
return matrix[attacker][defender]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCombination 获取元素组合(直接从预加载池读取)
|
// GetCombination 获取元素组合(直接按ID索引)
|
||||||
func (c *ElementCalculator) GetCombination(id int) (*ElementCombination, error) {
|
func (c *ElementCalculator) GetCombination(id int) (*ElementCombination, error) {
|
||||||
combo, exists := elementCombinationPool[id]
|
if !isValidCombinationID(id) {
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("invalid element combination ID: %d", id)
|
return nil, fmt.Errorf("invalid element combination ID: %d", id)
|
||||||
}
|
}
|
||||||
return combo, nil
|
return &elementCombinationPool[id], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOffensiveMultiplier 计算攻击方→防御方的克制倍数(缓存优先)
|
// GetOffensiveMultiplier 计算攻击方→防御方的克制倍数(只读查表)
|
||||||
func (c *ElementCalculator) GetOffensiveMultiplier(attackerID, defenderID int) (float64, error) {
|
func (c *ElementCalculator) GetOffensiveMultiplier(attackerID, defenderID int) (float64, error) {
|
||||||
// 1. 获取预加载的组合实例
|
if !isValidCombinationID(attackerID) {
|
||||||
attacker, err := c.GetCombination(attackerID)
|
return 0, fmt.Errorf("attacker invalid: invalid element combination ID: %d", attackerID)
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("attacker invalid: %w", err)
|
|
||||||
}
|
}
|
||||||
defender, err := c.GetCombination(defenderID)
|
if !isValidCombinationID(defenderID) {
|
||||||
if err != nil {
|
return 0, fmt.Errorf("defender invalid: invalid element combination ID: %d", defenderID)
|
||||||
return 0, fmt.Errorf("defender invalid: %w", err)
|
|
||||||
}
|
}
|
||||||
|
return c.offensiveTable[attackerID][defenderID], nil
|
||||||
// 2. 缓存键(全局唯一)
|
|
||||||
cacheKey := fmt.Sprintf("a%d_d%d", attackerID, defenderID)
|
|
||||||
if val, exists := c.offensiveCache[cacheKey]; exists {
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 核心计算+缓存
|
|
||||||
val := c.calculateMultiplier(attacker, defender)
|
|
||||||
c.offensiveCache[cacheKey] = val
|
|
||||||
return val, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateMultiplier 核心克制计算逻辑
|
// calculateMultiplier 核心克制计算逻辑
|
||||||
func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombination) float64 {
|
func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombination) float64 {
|
||||||
// 场景1:单→单
|
|
||||||
if !attacker.IsDual() && !defender.IsDual() {
|
if !attacker.IsDual() && !defender.IsDual() {
|
||||||
return c.getMatrixValue(attacker.Primary, defender.Primary)
|
return c.getMatrixValue(attacker.Primary, defender.Primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 场景2:单→双
|
|
||||||
if !attacker.IsDual() {
|
if !attacker.IsDual() {
|
||||||
y1, y2 := defender.Primary, *defender.Secondary
|
y1, y2 := defender.Primary, *defender.Secondary
|
||||||
m1 := c.getMatrixValue(attacker.Primary, y1)
|
m1 := c.getMatrixValue(attacker.Primary, y1)
|
||||||
m2 := c.getMatrixValue(attacker.Primary, y2)
|
m2 := c.getMatrixValue(attacker.Primary, y2)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case m1 == 2 && m2 == 2:
|
case m1 == 2 && m2 == 2:
|
||||||
return 4.0
|
return 4.0
|
||||||
@@ -333,12 +340,10 @@ func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 场景3:双→单
|
|
||||||
if !defender.IsDual() {
|
if !defender.IsDual() {
|
||||||
return c.calculateDualToSingle(attacker.Primary, *attacker.Secondary, defender.Primary)
|
return c.calculateDualToSingle(attacker.Primary, *attacker.Secondary, defender.Primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 场景4:双→双
|
|
||||||
x1, x2 := attacker.Primary, *attacker.Secondary
|
x1, x2 := attacker.Primary, *attacker.Secondary
|
||||||
y1, y2 := defender.Primary, *defender.Secondary
|
y1, y2 := defender.Primary, *defender.Secondary
|
||||||
coeffY1 := c.calculateDualToSingle(x1, x2, y1)
|
coeffY1 := c.calculateDualToSingle(x1, x2, y1)
|
||||||
@@ -350,7 +355,6 @@ func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombi
|
|||||||
func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender ElementType) float64 {
|
func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender ElementType) float64 {
|
||||||
k1 := c.getMatrixValue(attacker1, defender)
|
k1 := c.getMatrixValue(attacker1, defender)
|
||||||
k2 := c.getMatrixValue(attacker2, defender)
|
k2 := c.getMatrixValue(attacker2, defender)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case k1 == 2 && k2 == 2:
|
case k1 == 2 && k2 == 2:
|
||||||
return 4.0
|
return 4.0
|
||||||
@@ -361,60 +365,49 @@ func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var Calculator = NewElementCalculator()
|
|
||||||
|
|
||||||
// TestAllScenarios 全场景测试(验证预加载和计算逻辑)
|
// TestAllScenarios 全场景测试(验证预加载和计算逻辑)
|
||||||
func TestAllScenarios() {
|
func TestAllScenarios() {
|
||||||
|
|
||||||
// 测试1:单→单(草→水)
|
|
||||||
m1, _ := Calculator.GetOffensiveMultiplier(1, 2)
|
m1, _ := Calculator.GetOffensiveMultiplier(1, 2)
|
||||||
fmt.Println("草→水: %.2f(预期2.0)", m1)
|
fmt.Println("草→水: %.2f(预期2.0)", m1)
|
||||||
if math.Abs(m1-2.0) > 0.001 {
|
if math.Abs(m1-2.0) > 0.001 {
|
||||||
fmt.Println("测试1失败:实际%.2f", m1)
|
fmt.Println("测试1失败:实际%.2f", m1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试2:特殊单→单(混沌→虚空)
|
|
||||||
m2, _ := Calculator.GetOffensiveMultiplier(222, 226)
|
m2, _ := Calculator.GetOffensiveMultiplier(222, 226)
|
||||||
fmt.Println("混沌→虚空: %.2f(预期0.0)", m2)
|
fmt.Println("混沌→虚空: %.2f(预期0.0)", m2)
|
||||||
if math.Abs(m2-0.0) > 0.001 {
|
if math.Abs(m2-0.0) > 0.001 {
|
||||||
fmt.Println("测试2失败:实际%.2f", m2)
|
fmt.Println("测试2失败:实际%.2f", m2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试3:单→双(火→冰龙(43))
|
|
||||||
m3, _ := Calculator.GetOffensiveMultiplier(3, 43)
|
m3, _ := Calculator.GetOffensiveMultiplier(3, 43)
|
||||||
fmt.Println("火→冰龙: %.2f(预期1.5)", m3)
|
fmt.Println("火→冰龙: %.2f(预期1.5)", m3)
|
||||||
if math.Abs(m3-1.5) > 0.001 {
|
if math.Abs(m3-1.5) > 0.001 {
|
||||||
fmt.Println("测试3失败:实际%.2f", m3)
|
fmt.Println("测试3失败:实际%.2f", m3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试4:双→特殊单(混沌暗影(92)→神灵(223))
|
|
||||||
m4, _ := Calculator.GetOffensiveMultiplier(92, 223)
|
m4, _ := Calculator.GetOffensiveMultiplier(92, 223)
|
||||||
fmt.Println("混沌暗影→神灵: %.2f(预期1.25)", m4)
|
fmt.Println("混沌暗影→神灵: %.2f(预期1.25)", m4)
|
||||||
if math.Abs(m4-1.25) > 0.001 {
|
if math.Abs(m4-1.25) > 0.001 {
|
||||||
fmt.Println("测试4失败:实际%.2f", m4)
|
fmt.Println("测试4失败:实际%.2f", m4)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试5:双→双(虚空邪灵(113)→混沌远古(98))
|
|
||||||
m5, _ := Calculator.GetOffensiveMultiplier(113, 98)
|
m5, _ := Calculator.GetOffensiveMultiplier(113, 98)
|
||||||
fmt.Println("虚空邪灵→混沌远古: %.2f(预期0.875", m5)
|
fmt.Println("虚空邪灵→混沌远古: %.2f(预期0.875", m5)
|
||||||
if math.Abs(m5-0.875) > 0.001 {
|
if math.Abs(m5-0.875) > 0.001 {
|
||||||
fmt.Println("测试5失败:实际%.2f", m5)
|
fmt.Println("测试5失败:实际%.2f", m5)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试6:缓存命中
|
|
||||||
m6, _ := Calculator.GetOffensiveMultiplier(113, 98)
|
m6, _ := Calculator.GetOffensiveMultiplier(113, 98)
|
||||||
if math.Abs(m6-m5) > 0.001 {
|
if math.Abs(m6-m5) > 0.001 {
|
||||||
fmt.Println("测试6失败:缓存未命中")
|
fmt.Println("测试6失败:缓存未命中")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试7:含无效组合(电→地面)
|
|
||||||
m7, _ := Calculator.GetOffensiveMultiplier(5, 7)
|
m7, _ := Calculator.GetOffensiveMultiplier(5, 7)
|
||||||
fmt.Println("电→地面: %.2f(预期0.0)", m7)
|
fmt.Println("电→地面: %.2f(预期0.0)", m7)
|
||||||
if math.Abs(m7-0.0) > 0.001 {
|
if math.Abs(m7-0.0) > 0.001 {
|
||||||
fmt.Println("测试7失败:实际%.2f", m7)
|
fmt.Println("测试7失败:实际%.2f", m7)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试8:双属性含无效(电战斗→地面)
|
|
||||||
m8, _ := Calculator.GetOffensiveMultiplier(35, 7)
|
m8, _ := Calculator.GetOffensiveMultiplier(35, 7)
|
||||||
fmt.Println("电战斗→地面: %.2f(预期0.25)", m8)
|
fmt.Println("电战斗→地面: %.2f(预期0.25)", m8)
|
||||||
if math.Abs(m8-0.25) > 0.001 {
|
if math.Abs(m8-0.25) > 0.001 {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package data
|
package data
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/grand"
|
||||||
|
|
||||||
// 1. 质量枚举常量(保持不变)
|
// 1. 质量枚举常量(保持不变)
|
||||||
const (
|
const (
|
||||||
BitmapFilterQualityLow = 1 // LOW:应用1次滤镜
|
BitmapFilterQualityLow = 1 // LOW:应用1次滤镜
|
||||||
@@ -20,6 +22,35 @@ const (
|
|||||||
colorMax = 0xFFFFFF // 颜色值最大值(0xRRGGBB)
|
colorMax = 0xFFFFFF // 颜色值最大值(0xRRGGBB)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func GetDef() GlowFilter {
|
||||||
|
|
||||||
|
ret := GlowFilter{
|
||||||
|
// Color: 16777215, // 0xFFFFFF(对应JSON的color:16777215)
|
||||||
|
Alpha: 0.1, // 光圈大小,透明度
|
||||||
|
BlurX: 8, // 局外光圈大小
|
||||||
|
BlurY: 8, // 局外光圈大小
|
||||||
|
Strength: 8, // 颜色对比度
|
||||||
|
Quality: 1, // 背包内光圈大小
|
||||||
|
Inner: true, // 对应JSON的inner:true
|
||||||
|
Knockout: false, // 无JSON值,默认false
|
||||||
|
ColorMatrixFilter: [20]float32{1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}, // 对应JSON的matrix数组
|
||||||
|
Level: 2, // 对应JSON的level:"1"(转uint8)
|
||||||
|
}
|
||||||
|
ret.Color = RandomRGBToUint32()
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomRGBToUint32 生成随机RGB颜色并转为uint32(格式:0x00RRGGBB,最高8位留空)
|
||||||
|
func RandomRGBToUint32() uint32 {
|
||||||
|
// 生成0-255的随机R/G/B分量
|
||||||
|
r := uint32(grand.Intn(256))
|
||||||
|
g := uint32(grand.Intn(256))
|
||||||
|
b := uint32(grand.Intn(256))
|
||||||
|
|
||||||
|
// 位拼接:R左移16位,G左移8位,B不位移,组合成uint32
|
||||||
|
return (r << 16) | (g << 8) | b
|
||||||
|
}
|
||||||
|
|
||||||
// 精灵加shinylen字段
|
// 精灵加shinylen字段
|
||||||
// 3. 核心结构体:BlurX/BlurY/Strength 改为 uint8
|
// 3. 核心结构体:BlurX/BlurY/Strength 改为 uint8
|
||||||
type GlowFilter struct {
|
type GlowFilter struct {
|
||||||
@@ -53,6 +84,6 @@ type GlowFilter struct {
|
|||||||
// ItemInfo
|
// ItemInfo
|
||||||
// 用于表示发放物品的信息
|
// 用于表示发放物品的信息
|
||||||
type ItemInfo struct {
|
type ItemInfo struct {
|
||||||
ItemId uint32 `json:"itemId" description:"发放物品ID"` // 发放物品ID,
|
ItemId int64 `struc:"uint32"`
|
||||||
ItemCnt uint32 `json:"itemCount" description:"发放物品的数量"` // 发放物品的数量,
|
ItemCnt int64 `struc:"uint32"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func (s *cacheStore[T]) Del(ctx context.Context, key string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return gerror.Wrapf(err, "删除缓存失败,键: %s", key)
|
return gerror.Wrapf(err, "删除缓存失败,键: %s", key)
|
||||||
}
|
}
|
||||||
fmt.Printf("[INFO] 删除缓存 [%s] 键: %s 成功\n", s.prefix, key)
|
//fmt.Printf("[INFO] 删除缓存 [%s] 键: %s 成功\n", s.prefix, key)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ func newSessionStore() *cacheStore[uint32] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newUserOnlineStore 创建用户在线状态缓存实例
|
// newUserOnlineStore 创建用户在线状态缓存实例
|
||||||
func newUserOnlineStore() *cacheStore[uint16] {
|
func newUserOnlineStore() *cacheStore[uint32] {
|
||||||
return &cacheStore[uint16]{
|
return &cacheStore[uint32]{
|
||||||
manager: cool.CacheManager,
|
manager: cool.CacheManager,
|
||||||
prefix: "blazing:useronline:",
|
prefix: "blazing:useronline:",
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ func newEmailCodeStore() *cacheStore[int] {
|
|||||||
// sessionManager 会话管理器
|
// sessionManager 会话管理器
|
||||||
type sessionManager struct {
|
type sessionManager struct {
|
||||||
sessionStore *cacheStore[uint32] // 会话缓存
|
sessionStore *cacheStore[uint32] // 会话缓存
|
||||||
userOnlineStore *cacheStore[uint16] // 用户在线状态缓存
|
userOnlineStore *cacheStore[uint32] // 用户在线状态缓存
|
||||||
emailCodeStore *cacheStore[int] // 邮件注册码缓存
|
emailCodeStore *cacheStore[int] // 邮件注册码缓存
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +52,12 @@ func newSessionManager() *sessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetUserOnline 设置用户在线状态
|
// SetUserOnline 设置用户在线状态
|
||||||
func (m *sessionManager) SetUserOnline(userID uint32, serverID uint16) error {
|
func (m *sessionManager) SetUserOnline(userID uint32, serverID uint32) error {
|
||||||
return m.userOnlineStore.Set(gctx.New(), gconv.String(userID), serverID, 0)
|
return m.userOnlineStore.Set(gctx.New(), gconv.String(userID), serverID, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserOnline 获取用户在线状态
|
// GetUserOnline 获取用户在线状态
|
||||||
func (m *sessionManager) GetUserOnline(userID uint32) (uint16, error) {
|
func (m *sessionManager) GetUserOnline(userID uint32) (uint32, error) {
|
||||||
return m.userOnlineStore.Get(context.Background(), gconv.String(userID))
|
return m.userOnlineStore.Get(context.Background(), gconv.String(userID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ type EffectArg struct {
|
|||||||
SideEffect []struct {
|
SideEffect []struct {
|
||||||
ID int `json:"ID"`
|
ID int `json:"ID"`
|
||||||
SideEffectArgcount int `json:"SideEffectArgcount"`
|
SideEffectArgcount int `json:"SideEffectArgcount"`
|
||||||
SideEffectArg string `json:"SideEffectArg,omitempty"`
|
SideEffectArg rawFlexibleString `json:"SideEffectArg,omitempty"`
|
||||||
} `json:"SideEffect"`
|
} `json:"SideEffect"`
|
||||||
} `json:"SideEffects"`
|
} `json:"SideEffects"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
_ "blazing/common/data/xmlres/packed"
|
_ "blazing/common/data/xmlres/packed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ECUST-XX/xml"
|
"github.com/ECUST-XX/xml"
|
||||||
"github.com/gogf/gf/v2/os/gres"
|
"github.com/gogf/gf/v2/os/gres"
|
||||||
@@ -14,22 +14,36 @@ import (
|
|||||||
|
|
||||||
var path string
|
var path string
|
||||||
|
|
||||||
|
func readConfigContent(path string) []byte {
|
||||||
|
return gres.GetContent(path)
|
||||||
|
}
|
||||||
|
|
||||||
func getXml[T any](path string) T {
|
func getXml[T any](path string) T {
|
||||||
|
|
||||||
// 解析XML到结构体
|
// 解析XML到结构体
|
||||||
var xmls T
|
var xmls T
|
||||||
|
|
||||||
t1 := gres.GetContent(path)
|
t1 := readConfigContent(path)
|
||||||
xml.Unmarshal(t1, &xmls)
|
xml.Unmarshal(t1, &xmls)
|
||||||
|
|
||||||
return xmls
|
return xmls
|
||||||
}
|
}
|
||||||
func getJson[T any](path string) T {
|
func getJson[T any](path string) T {
|
||||||
|
|
||||||
// 解析XML到结构体
|
// 解析JSON到结构体
|
||||||
var xmls T
|
var xmls T
|
||||||
t1 := gres.GetContent(path)
|
t1 := readConfigContent(path)
|
||||||
json.Unmarshal(t1, &xmls)
|
if len(t1) == 0 {
|
||||||
|
fmt.Printf("[xmlres] getJson empty content: path=%s\n", path)
|
||||||
|
return xmls
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(t1, &xmls); err != nil {
|
||||||
|
head := string(t1)
|
||||||
|
if len(head) > 300 {
|
||||||
|
head = head[:300]
|
||||||
|
}
|
||||||
|
fmt.Printf("[xmlres] getJson unmarshal failed: path=%s len=%d err=%v head=%q\n", path, len(t1), err, head)
|
||||||
|
}
|
||||||
|
|
||||||
return xmls
|
return xmls
|
||||||
}
|
}
|
||||||
@@ -40,7 +54,7 @@ var (
|
|||||||
// EffectArgsConfig EffectArg //arg参数
|
// EffectArgsConfig EffectArg //arg参数
|
||||||
//TalkConfig TalkRoot //任务配置
|
//TalkConfig TalkRoot //任务配置
|
||||||
// //Monster MonsterRoot //野怪配置
|
// //Monster MonsterRoot //野怪配置
|
||||||
MonsterMap map[int]TMapConfig
|
//MonsterMap map[int]TMapConfig
|
||||||
//Skill MovesTbl //技能配置
|
//Skill MovesTbl //技能配置
|
||||||
SkillMap map[int]Move
|
SkillMap map[int]Move
|
||||||
PetMAP map[int]PetInfo //宠物配置
|
PetMAP map[int]PetInfo //宠物配置
|
||||||
@@ -58,8 +72,6 @@ var (
|
|||||||
|
|
||||||
func Initfile() {
|
func Initfile() {
|
||||||
//gres.Dump()
|
//gres.Dump()
|
||||||
path1, _ := os.Getwd()
|
|
||||||
path = path1 + "/public/config/"
|
|
||||||
path = "config/"
|
path = "config/"
|
||||||
MapConfig = getXml[Maps](path + "210.xml")
|
MapConfig = getXml[Maps](path + "210.xml")
|
||||||
|
|
||||||
@@ -78,19 +90,19 @@ func Initfile() {
|
|||||||
})
|
})
|
||||||
//TalkConfig = getXml[TalkRoot](path + "talk.xml")
|
//TalkConfig = getXml[TalkRoot](path + "talk.xml")
|
||||||
|
|
||||||
MonsterMap = utils.ToMap(getXml[MonsterRoot](path+"地图配置野怪.xml").Maps, func(m TMapConfig) int {
|
// MonsterMap = utils.ToMap(getXml[MonsterRoot](path+"地图配置野怪.xml").Maps, func(m TMapConfig) int {
|
||||||
return m.ID
|
// return m.ID
|
||||||
|
|
||||||
})
|
// })
|
||||||
|
|
||||||
ShopMap = utils.ToMap(getXml[ShopRoot](path+"地图配置野怪.xml").Items, func(m ShopItem) int {
|
ShopMap = utils.ToMap(getXml[ShopRoot](path+"地图配置野怪.xml").Items, func(m ShopItem) int {
|
||||||
return gconv.Int(m.ProductID)
|
return gconv.Int(m.ProductID)
|
||||||
|
|
||||||
})
|
})
|
||||||
Skill := getXml[MovesTbl](path + "227.xml")
|
skillConfig := getJson[MovesJSON](path + "moves_flash.json")
|
||||||
|
|
||||||
SkillMap = make(map[int]Move, len(Skill.Moves))
|
SkillMap = make(map[int]Move, len(skillConfig.MovesTbl.Moves.Move))
|
||||||
for _, v := range Skill.Moves {
|
for _, v := range skillConfig.MovesTbl.Moves.Move {
|
||||||
v.SideEffectS = ParseSideEffectArgs(v.SideEffect)
|
v.SideEffectS = ParseSideEffectArgs(v.SideEffect)
|
||||||
v.SideEffectArgS = ParseSideEffectArgs(v.SideEffectArg)
|
v.SideEffectArgS = ParseSideEffectArgs(v.SideEffectArg)
|
||||||
SkillMap[v.ID] = v
|
SkillMap[v.ID] = v
|
||||||
@@ -101,7 +113,11 @@ func Initfile() {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
PetMAP = utils.ToMap[PetInfo, int](getXml[Monsters](path+"226.xml").Monsters, func(m PetInfo) int {
|
pets := getXml[Monsters](path + "226.xml").Monsters
|
||||||
|
for i := range pets {
|
||||||
|
pets[i].YieldingEVValues = parseYieldingEV(pets[i].YieldingEV)
|
||||||
|
}
|
||||||
|
PetMAP = utils.ToMap[PetInfo, int](pets, func(m PetInfo) int {
|
||||||
return m.ID
|
return m.ID
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,23 +11,24 @@ type Items struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Item struct {
|
type Item struct {
|
||||||
ID int `xml:"ID,attr"` // 物品ID(与items.xml一致)
|
ID int `xml:"ID,attr"` // 物品ID(与items.xml一致)
|
||||||
Name string `xml:"Name,attr"` // 物品名称
|
Name string `xml:"Name,attr"` // 物品名称
|
||||||
Rarity int `xml:"Rarity,attr,omitempty"` // 稀有度
|
Rarity int `xml:"Rarity,attr,omitempty"` // 稀有度
|
||||||
ItemType int `xml:"ItemType,attr"` // 物品类型(0:胶囊 1:体力药剂 2:活力药剂)
|
ItemType int `xml:"ItemType,attr"` // 物品类型(0:胶囊 1:体力药剂 2:活力药剂)
|
||||||
Max int `xml:"Max,attr"` // 最大堆叠数量
|
Max int `xml:"Max,attr"` // 最大堆叠数量
|
||||||
Price int `xml:"Price,attr"` // 价格
|
Price int `xml:"Price,attr"` // 价格
|
||||||
Bonus float64 `xml:"Bonus,attr,omitempty"` // 倍率(如捕捉胶囊的加成倍数,修正为浮点型)
|
Bonus float64 `xml:"Bonus,attr,omitempty"` // 倍率(如捕捉胶囊的加成倍数,修正为浮点型)
|
||||||
Tradability int `xml:"Tradability,attr"` // 可交易性(0/1)
|
Tradability int `xml:"Tradability,attr"` // 可交易性(0/1)
|
||||||
VipTradability int `xml:"VipTradability,attr"` // VIP可交易性(0/1)
|
VipTradability int `xml:"VipTradability,attr"` // VIP可交易性(0/1)
|
||||||
DailyKey int `xml:"DailyKey,attr,omitempty"` // 每日限制键值
|
DailyKey int `xml:"DailyKey,attr,omitempty"` // 每日限制键值
|
||||||
DailyOutMax int `xml:"DailyOutMax,attr,omitempty"` // 每日最大产出
|
DailyOutMax int `xml:"DailyOutMax,attr,omitempty"` // 每日最大产出
|
||||||
Wd int `xml:"wd,attr"` // 未知属性
|
Wd int `xml:"wd,attr"` // 未知属性
|
||||||
UseMax int `xml:"UseMax,attr"` // 最大使用次数
|
UseMax int `xml:"UseMax,attr"` // 最大使用次数
|
||||||
LifeTime int `xml:"LifeTime,attr"` // 生命周期(0为永久)
|
LifeTime int `xml:"LifeTime,attr"` // 生命周期(0为永久)
|
||||||
Purpose int `xml:"purpose,attr"` // 用途标识
|
Purpose int `xml:"purpose,attr"` // 用途标识
|
||||||
Bean int `xml:"Bean,attr,omitempty"` // 豆子数量
|
Bean int `xml:"Bean,attr,omitempty"` // 豆子数量
|
||||||
Hide int `xml:"Hide,attr"` // 是否隐藏(0/1)
|
Hide int `xml:"Hide,attr"` // 是否隐藏(0/1)
|
||||||
|
Texture int `xml:"Texture,attr,omitempty"`
|
||||||
Sort int `xml:"Sort,attr,omitempty"` // 排序序号
|
Sort int `xml:"Sort,attr,omitempty"` // 排序序号
|
||||||
Des string `xml:"des,attr,omitempty"` // 物品用途(XML中无该属性,保留字段供自定义)
|
Des string `xml:"des,attr,omitempty"` // 物品用途(XML中无该属性,保留字段供自定义)
|
||||||
Color string `xml:"color,attr,omitempty"` // 装备名字颜色
|
Color string `xml:"color,attr,omitempty"` // 装备名字颜色
|
||||||
|
|||||||
26
common/data/xmlres/json_compat_test.go
Normal file
26
common/data/xmlres/json_compat_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package xmlres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMoveUnmarshalJSONAcceptsNumericName(t *testing.T) {
|
||||||
|
var move Move
|
||||||
|
if err := json.Unmarshal([]byte(`{"ID":10001,"Name":1,"Category":1,"Type":8,"Power":35,"MaxPP":35,"Accuracy":95}`), &move); err != nil {
|
||||||
|
t.Fatalf("unmarshal move failed: %v", err)
|
||||||
|
}
|
||||||
|
if move.Name != "1" {
|
||||||
|
t.Fatalf("expected numeric name to convert to string, got %q", move.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectArgUnmarshalJSONAcceptsNumericSideEffectArg(t *testing.T) {
|
||||||
|
var cfg EffectArg
|
||||||
|
if err := json.Unmarshal([]byte(`{"SideEffects":{"SideEffect":[{"ID":1,"SideEffectArgcount":1,"SideEffectArg":3}]}}`), &cfg); err != nil {
|
||||||
|
t.Fatalf("unmarshal effect arg failed: %v", err)
|
||||||
|
}
|
||||||
|
if got := string(cfg.SideEffects.SideEffect[0].SideEffectArg); got != "3" {
|
||||||
|
t.Fatalf("expected numeric side effect arg to convert to string, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,11 @@
|
|||||||
package xmlres
|
package xmlres
|
||||||
|
|
||||||
import "github.com/ECUST-XX/xml"
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ECUST-XX/xml"
|
||||||
|
)
|
||||||
|
|
||||||
// Move 表示怪物可学习的技能
|
// Move 表示怪物可学习的技能
|
||||||
type PetMoves struct {
|
type PetMoves struct {
|
||||||
@@ -15,28 +20,28 @@ type LearnableMoves struct {
|
|||||||
|
|
||||||
// PetInfo 表示一个怪物的信息
|
// PetInfo 表示一个怪物的信息
|
||||||
type PetInfo struct {
|
type PetInfo struct {
|
||||||
ID int `xml:"ID,attr"`
|
ID int `xml:"ID,attr"`
|
||||||
DefName string `xml:"DefName,attr"` // 名字
|
DefName string `xml:"DefName,attr"` // 名字
|
||||||
Type int `xml:"Type,attr"` // 类型
|
Type int `xml:"Type,attr"` // 类型
|
||||||
IsLarge int `xml:"IsLarge,attr"` // 是否为大型怪物
|
IsLarge int `xml:"IsLarge,attr"` // 是否为大型怪物
|
||||||
GrowthType int `xml:"GrowthType,attr"` // 成长类型
|
GrowthType int `xml:"GrowthType,attr"` // 成长类型
|
||||||
HP int `xml:"HP,attr"` // 血量种族值
|
HP int `xml:"HP,attr"` // 血量种族值
|
||||||
Atk uint32 `xml:"Atk,attr"` // 攻击种族值
|
Atk uint32 `xml:"Atk,attr"` // 攻击种族值
|
||||||
Def uint32 `xml:"Def,attr"` // 防御种族值
|
Def uint32 `xml:"Def,attr"` // 防御种族值
|
||||||
SpAtk uint32 `xml:"SpAtk,attr"` // 特殊攻击种族值
|
SpAtk uint32 `xml:"SpAtk,attr"` // 特殊攻击种族值
|
||||||
SpDef uint32 `xml:"SpDef,attr"` // 特殊防御种族值
|
SpDef uint32 `xml:"SpDef,attr"` // 特殊防御种族值
|
||||||
Spd uint32 `xml:"Spd,attr"` // 速度种族值
|
Spd uint32 `xml:"Spd,attr"` // 速度种族值
|
||||||
YieldingExp int `xml:"YieldingExp,attr"` // 击败后获得的经验值
|
YieldingExp int `xml:"YieldingExp,attr"` // 击败后获得的经验值
|
||||||
CatchRate int `xml:"CatchRate,attr"` // 捕捉率
|
CatchRate int `xml:"CatchRate,attr"` // 捕捉率
|
||||||
YieldingEV string `xml:"YieldingEV,attr"` // 努力值奖励,格式为"HP Atk Def SpAtk SpDef Spd"
|
YieldingEV string `xml:"YieldingEV,attr"` // 努力值奖励,格式为"HP Atk Def SpAtk SpDef Spd"
|
||||||
EvolvesFrom int `xml:"EvolvesFrom,attr"` // 进化前的怪物ID
|
EvolvesFrom int `xml:"EvolvesFrom,attr"` // 进化前的怪物ID
|
||||||
EvolvesTo uint32 `xml:"EvolvesTo,attr"` // 进化后的怪物ID
|
EvolvesTo uint32 `xml:"EvolvesTo,attr"` // 进化后的怪物ID
|
||||||
EvolvFlag int `xml:"EvolvFlag,attr"` //<!-- EvolvFlag: 0 - 直接进化(等级到了就进化); 1~49 - 触发进化,默认值: 0 (默认直接进化) -->
|
EvolvFlag int `xml:"EvolvFlag,attr"` //<!-- EvolvFlag: 0 - 直接进化(等级到了就进化); 1~49 - 触发进化,默认值: 0 (默认直接进化) -->
|
||||||
EvolvingLv int `xml:"EvolvingLv,attr"` // 进化等级
|
EvolvingLv int `xml:"EvolvingLv,attr"` // 进化等级
|
||||||
FreeForbidden int `xml:"FreeForbidden,attr"` // 是否禁止放生
|
FreeForbidden int `xml:"FreeForbidden,attr"` // 是否禁止放生
|
||||||
FuseMaster int `xml:"FuseMaster,attr"` // 是否可作为融合主素材
|
FuseMaster int `xml:"FuseMaster,attr"` // 是否可作为融合主素材
|
||||||
FuseSub int `xml:"FuseSub,attr"` // 是否可作为融合副素材
|
FuseSub int `xml:"FuseSub,attr"` // 是否可作为融合副素材
|
||||||
Gender int `xml:"Gender,attr"` // 性别 0-无性别 1-雄性 2-雌性
|
// Gender int `xml:"Gender,attr"` // 性别 0-无性别 1-雄性 2-雌性
|
||||||
PetClass int `xml:"PetClass,attr"` // 宠物类别
|
PetClass int `xml:"PetClass,attr"` // 宠物类别
|
||||||
FormParam float64 `xml:"FormParam,attr"` // 形态参数
|
FormParam float64 `xml:"FormParam,attr"` // 形态参数
|
||||||
CharacterAttrParam int `xml:"CharacterAttrParam,attr"` // 特性参数
|
CharacterAttrParam int `xml:"CharacterAttrParam,attr"` // 特性参数
|
||||||
@@ -45,6 +50,7 @@ type PetInfo struct {
|
|||||||
Recycle int `xml:"Recycle,attr"` // 是否可回收
|
Recycle int `xml:"Recycle,attr"` // 是否可回收
|
||||||
LearnableMoves LearnableMoves `xml:"LearnableMoves"` // 可学习的技能
|
LearnableMoves LearnableMoves `xml:"LearnableMoves"` // 可学习的技能
|
||||||
NaturalEnemy string `xml:"NaturalEnemy,attr"` //天敌
|
NaturalEnemy string `xml:"NaturalEnemy,attr"` //天敌
|
||||||
|
YieldingEVValues []int64 `xml:"-"` // 预解析后的努力值奖励
|
||||||
}
|
}
|
||||||
|
|
||||||
func (basic *PetInfo) GetBasic() uint32 {
|
func (basic *PetInfo) GetBasic() uint32 {
|
||||||
@@ -61,3 +67,16 @@ type Monsters struct {
|
|||||||
XMLName xml.Name `xml:"Monsters"`
|
XMLName xml.Name `xml:"Monsters"`
|
||||||
Monsters []PetInfo `xml:"Monster"`
|
Monsters []PetInfo `xml:"Monster"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseYieldingEV(raw string) []int64 {
|
||||||
|
values := make([]int64, 6)
|
||||||
|
parts := strings.Fields(raw)
|
||||||
|
for i := 0; i < len(parts) && i < len(values); i++ {
|
||||||
|
value, err := strconv.ParseInt(parts[i], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values[i] = value
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package xmlres
|
package xmlres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -33,51 +34,156 @@ type MovesTbl struct {
|
|||||||
Moves []Move `xml:"Moves>Move"`
|
Moves []Move `xml:"Moves>Move"`
|
||||||
EFF []SideEffect `xml:"SideEffects>SideEffect"`
|
EFF []SideEffect `xml:"SideEffects>SideEffect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MovesJSON struct {
|
||||||
|
MovesTbl MovesJSONRoot `json:"MovesTbl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MovesJSONRoot struct {
|
||||||
|
Moves struct {
|
||||||
|
Move []Move `json:"Move"`
|
||||||
|
} `json:"Moves"`
|
||||||
|
SideEffects struct {
|
||||||
|
SideEffect []SideEffect `json:"SideEffect"`
|
||||||
|
} `json:"SideEffects"`
|
||||||
|
}
|
||||||
|
|
||||||
type MovesMap struct {
|
type MovesMap struct {
|
||||||
XMLName xml.Name `xml:"MovesTbl"`
|
XMLName xml.Name `xml:"MovesTbl"`
|
||||||
Moves map[int]Move
|
Moves map[int]Move
|
||||||
EFF []SideEffect `xml:"SideEffects>SideEffect"`
|
EFF []SideEffect `xml:"SideEffects>SideEffect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rawFlexibleString string
|
||||||
|
|
||||||
|
func (s *rawFlexibleString) UnmarshalJSON(data []byte) error {
|
||||||
|
text := strings.TrimSpace(string(data))
|
||||||
|
if text == "" || text == "null" {
|
||||||
|
*s = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(text) >= 2 && text[0] == '"' && text[len(text)-1] == '"' {
|
||||||
|
var decoded string
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*s = rawFlexibleString(decoded)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = rawFlexibleString(text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Move 定义单个技能的结构
|
// Move 定义单个技能的结构
|
||||||
type Move struct {
|
type Move struct {
|
||||||
ID int `xml:"ID,attr"`
|
ID int `xml:"ID,attr" json:"ID"`
|
||||||
Name string `xml:"Name,attr"`
|
Name string `xml:"Name,attr" json:"Name"`
|
||||||
|
|
||||||
Category int `xml:"Category,attr"` //属性
|
Category int `xml:"Category,attr" json:"Category"` //属性
|
||||||
Type int `xml:"Type,attr"` //类型
|
Type int `xml:"Type,attr" json:"Type"` //类型
|
||||||
Power int `xml:"Power,attr"` //威力
|
Power int `xml:"Power,attr" json:"Power"` //威力
|
||||||
MaxPP int `xml:"MaxPP,attr"` //最大PP
|
MaxPP int `xml:"MaxPP,attr" json:"MaxPP"` //最大PP
|
||||||
Accuracy int `xml:"Accuracy,attr"` //命中率
|
Accuracy int `xml:"Accuracy,attr" json:"Accuracy"` //命中率
|
||||||
CritRate int `xml:"CritRate,attr,omitempty"` //暴击率
|
CritRate int `xml:"CritRate,attr,omitempty" json:"CritRate,omitempty"` //暴击率
|
||||||
Priority int `xml:"Priority,attr,omitempty"` //优先级
|
Priority int `xml:"Priority,attr,omitempty" json:"Priority,omitempty"` //优先级
|
||||||
MustHit int `xml:"MustHit,attr,omitempty"` //是否必中
|
MustHit int `xml:"MustHit,attr,omitempty" json:"MustHit,omitempty"` //是否必中
|
||||||
SwapElemType int `xml:"SwapElemType,attr,omitempty"` //技能交换属性
|
SwapElemType int `xml:"SwapElemType,attr,omitempty" json:"SwapElemType,omitempty"` //技能交换属性
|
||||||
CopyElemType int `xml:"CopyElemType,attr,omitempty"` // 技能复制属性
|
CopyElemType int `xml:"CopyElemType,attr,omitempty" json:"CopyElemType,omitempty"` // 技能复制属性
|
||||||
CritAtkFirst int `xml:"CritAtkFirst,attr,omitempty"` // 先出手时必定致命一击
|
CritAtkFirst int `xml:"CritAtkFirst,attr,omitempty" json:"CritAtkFirst,omitempty"` // 先出手时必定致命一击
|
||||||
CritAtkSecond int `xml:"CritAtkSecond,attr,omitempty"` //后出手时必定致命一击
|
CritAtkSecond int `xml:"CritAtkSecond,attr,omitempty" json:"CritAtkSecond,omitempty"` //后出手时必定致命一击
|
||||||
CritSelfHalfHp int `xml:"CritSelfHalfHp,attr,omitempty"` //自身体力低于一半时必定致命一击
|
CritSelfHalfHp int `xml:"CritSelfHalfHp,attr,omitempty" json:"CritSelfHalfHp,omitempty"` //自身体力低于一半时必定致命一击
|
||||||
CritFoeHalfHp int `xml:"CritFoeHalfHp,attr,omitempty"` //对方体力低于一半时必定致命一击
|
CritFoeHalfHp int `xml:"CritFoeHalfHp,attr,omitempty" json:"CritFoeHalfHp,omitempty"` //对方体力低于一半时必定致命一击
|
||||||
DmgBindLv int `xml:"DmgBindLv,attr,omitempty"` //使对方受到的伤害值等于自身的等级
|
DmgBindLv int `xml:"DmgBindLv,attr,omitempty" json:"DmgBindLv,omitempty"` //使对方受到的伤害值等于自身的等级
|
||||||
PwrBindDv int `xml:"PwrBindDv,attr,omitempty"` //威力(power)取决于自身的潜力(个体值)
|
PwrBindDv int `xml:"PwrBindDv,attr,omitempty" json:"PwrBindDv,omitempty"` //威力(power)取决于自身的潜力(个体值)
|
||||||
PwrDouble int `xml:"PwrDouble,attr,omitempty"` //攻击时,若对方处于异常状态, 则威力翻倍;
|
PwrDouble int `xml:"PwrDouble,attr,omitempty" json:"PwrDouble,omitempty"` //攻击时,若对方处于异常状态, 则威力翻倍;
|
||||||
DmgBindHpDv int `xml:"DmgBindHpDv,attr,omitempty"` //使对方受到的伤害值等于自身的体力值
|
DmgBindHpDv int `xml:"DmgBindHpDv,attr,omitempty" json:"DmgBindHpDv,omitempty"` //使对方受到的伤害值等于自身的体力值
|
||||||
SideEffect string `xml:"SideEffect,attr,omitempty"`
|
SideEffect string `xml:"SideEffect,attr,omitempty" json:"SideEffect,omitempty"`
|
||||||
SideEffectArg string `xml:"SideEffectArg,attr,omitempty"`
|
SideEffectArg string `xml:"SideEffectArg,attr,omitempty" json:"SideEffectArg,omitempty"`
|
||||||
SideEffectS []int
|
SideEffectS []int
|
||||||
SideEffectArgS []int
|
SideEffectArgS []int
|
||||||
AtkNum int `xml:"AtkNum,attr,omitempty"`
|
AtkNum int `xml:"AtkNum,attr,omitempty" json:"AtkNum,omitempty"`
|
||||||
Url string `xml:"Url,attr,omitempty"`
|
AtkType int `xml:"AtkType,attr,omitempty" json:"AtkType,omitempty"` // 0:所有人 1:仅己方 2:仅对方 3:仅自己
|
||||||
|
Url string `xml:"Url,attr,omitempty" json:"Url,omitempty"`
|
||||||
|
|
||||||
Info string `xml:"info,attr,omitempty"`
|
Info string `xml:"info,attr,omitempty" json:"info,omitempty"`
|
||||||
|
|
||||||
CD *int `xml:"CD,attr"`
|
CD *int `xml:"CD,attr" json:"CD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Move) UnmarshalJSON(data []byte) error {
|
||||||
|
type moveAlias struct {
|
||||||
|
ID int `json:"ID"`
|
||||||
|
Name rawFlexibleString `json:"Name"`
|
||||||
|
Category int `json:"Category"`
|
||||||
|
Type int `json:"Type"`
|
||||||
|
Power int `json:"Power"`
|
||||||
|
MaxPP int `json:"MaxPP"`
|
||||||
|
Accuracy int `json:"Accuracy"`
|
||||||
|
CritRate int `json:"CritRate,omitempty"`
|
||||||
|
Priority int `json:"Priority,omitempty"`
|
||||||
|
MustHit int `json:"MustHit,omitempty"`
|
||||||
|
SwapElemType int `json:"SwapElemType,omitempty"`
|
||||||
|
CopyElemType int `json:"CopyElemType,omitempty"`
|
||||||
|
CritAtkFirst int `json:"CritAtkFirst,omitempty"`
|
||||||
|
CritAtkSecond int `json:"CritAtkSecond,omitempty"`
|
||||||
|
CritSelfHalfHp int `json:"CritSelfHalfHp,omitempty"`
|
||||||
|
CritFoeHalfHp int `json:"CritFoeHalfHp,omitempty"`
|
||||||
|
DmgBindLv int `json:"DmgBindLv,omitempty"`
|
||||||
|
PwrBindDv int `json:"PwrBindDv,omitempty"`
|
||||||
|
PwrDouble int `json:"PwrDouble,omitempty"`
|
||||||
|
DmgBindHpDv int `json:"DmgBindHpDv,omitempty"`
|
||||||
|
SideEffect rawFlexibleString `json:"SideEffect,omitempty"`
|
||||||
|
SideEffectArg rawFlexibleString `json:"SideEffectArg,omitempty"`
|
||||||
|
AtkNum int `json:"AtkNum,omitempty"`
|
||||||
|
AtkType int `json:"AtkType,omitempty"`
|
||||||
|
Url string `json:"Url,omitempty"`
|
||||||
|
Info string `json:"info,omitempty"`
|
||||||
|
CD *int `json:"CD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var aux moveAlias
|
||||||
|
if err := json.Unmarshal(data, &aux); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*m = Move{
|
||||||
|
ID: aux.ID,
|
||||||
|
Name: string(aux.Name),
|
||||||
|
Category: aux.Category,
|
||||||
|
Type: aux.Type,
|
||||||
|
Power: aux.Power,
|
||||||
|
MaxPP: aux.MaxPP,
|
||||||
|
Accuracy: aux.Accuracy,
|
||||||
|
CritRate: aux.CritRate,
|
||||||
|
Priority: aux.Priority,
|
||||||
|
MustHit: aux.MustHit,
|
||||||
|
SwapElemType: aux.SwapElemType,
|
||||||
|
CopyElemType: aux.CopyElemType,
|
||||||
|
CritAtkFirst: aux.CritAtkFirst,
|
||||||
|
CritAtkSecond: aux.CritAtkSecond,
|
||||||
|
CritSelfHalfHp: aux.CritSelfHalfHp,
|
||||||
|
CritFoeHalfHp: aux.CritFoeHalfHp,
|
||||||
|
DmgBindLv: aux.DmgBindLv,
|
||||||
|
PwrBindDv: aux.PwrBindDv,
|
||||||
|
PwrDouble: aux.PwrDouble,
|
||||||
|
DmgBindHpDv: aux.DmgBindHpDv,
|
||||||
|
SideEffect: string(aux.SideEffect),
|
||||||
|
SideEffectArg: string(aux.SideEffectArg),
|
||||||
|
AtkNum: aux.AtkNum,
|
||||||
|
AtkType: aux.AtkType,
|
||||||
|
Url: aux.Url,
|
||||||
|
Info: aux.Info,
|
||||||
|
CD: aux.CD,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type SideEffect struct {
|
type SideEffect struct {
|
||||||
ID int `xml:"ID,attr"`
|
ID int `xml:"ID,attr" json:"ID"`
|
||||||
Help string `xml:"help,attr"`
|
Help string `xml:"help,attr" json:"help"`
|
||||||
Des string `xml:"des,attr"`
|
Des string `xml:"des,attr" json:"des"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadHTTPFile 通过HTTP GET请求获取远程文件内容
|
// ReadHTTPFile 通过HTTP GET请求获取远程文件内容
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module blazing/common
|
module blazing/common
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/panjf2000/gnet v1.6.7
|
github.com/panjf2000/gnet v1.6.7
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func GetServerInfoList(isdebug int32) []ServerInfo {
|
|||||||
|
|
||||||
}
|
}
|
||||||
tt.Name = v.Name
|
tt.Name = v.Name
|
||||||
tt.Port = v.Port
|
tt.Port =uint16( v.Port)
|
||||||
ret1 = append(ret1, *tt)
|
ret1 = append(ret1, *tt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ type ServerInfo struct {
|
|||||||
// 服务器IP, 16字节UTF-8, 不足16补齐到16
|
// 服务器IP, 16字节UTF-8, 不足16补齐到16
|
||||||
IP string `struc:"[16]byte"` // 定长模式:16字节
|
IP string `struc:"[16]byte"` // 定长模式:16字节
|
||||||
// 端口
|
// 端口
|
||||||
Port uint16
|
Port uint16
|
||||||
// 好友在线的个数
|
// 好友在线的个数
|
||||||
Friends uint32
|
Friends uint32
|
||||||
}
|
}
|
||||||
|
|||||||
237
common/rpc/func.go
Normal file
237
common/rpc/func.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/cool"
|
||||||
|
"blazing/logic/service/fight/pvp"
|
||||||
|
"blazing/logic/service/fight/pvpwire"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gredis"
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListenFunc 监听函数
|
||||||
|
// ListenFunc 改造后的 Redis PubSub 监听函数,支持自动重连。
|
||||||
|
// 注意:PubSub 连接只负责订阅和接收,避免在同一连接上并发 PING。
|
||||||
|
func ListenFunc(ctx g.Ctx) {
|
||||||
|
if !cool.IsRedisMode {
|
||||||
|
panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义常量配置
|
||||||
|
const (
|
||||||
|
subscribeTopic = "cool:func" // 订阅的主题
|
||||||
|
retryDelay = 10 * time.Second // 连接失败重试间隔
|
||||||
|
)
|
||||||
|
|
||||||
|
// 外层循环:负责连接断开后的整体重连
|
||||||
|
for {
|
||||||
|
// 检查上下文是否已取消,优雅退出
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
cool.Logger.Info(ctx, "ListenFunc 上下文已取消,退出监听")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 建立 Redis 连接
|
||||||
|
conn, err := cool.Redis.Conn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
cool.Logger.Error(ctx, "获取 Redis 连接失败", "error", err, "retry_after", retryDelay)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 订阅主题
|
||||||
|
_, err = conn.Do(ctx, "subscribe", subscribeTopic)
|
||||||
|
if err != nil {
|
||||||
|
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", subscribeTopic, "error", err)
|
||||||
|
_ = conn.Close(ctx)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", subscribeTopic)
|
||||||
|
_, err = conn.Do(ctx, "subscribe", "sun:join") //加入队列
|
||||||
|
if err != nil {
|
||||||
|
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", "sun:join", "error", err)
|
||||||
|
_ = conn.Close(ctx)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", "sun:join")
|
||||||
|
|
||||||
|
// 3. 循环接收消息
|
||||||
|
connError := false
|
||||||
|
for !connError {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
connError = true
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收消息
|
||||||
|
data, err := conn.Receive(ctx)
|
||||||
|
if err != nil {
|
||||||
|
cool.Logger.Error(ctx, "Redis PubSub Receive 失败", "error", err)
|
||||||
|
connError = true // 标记连接错误,触发重连
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理消息(保留原有业务逻辑)
|
||||||
|
if data != nil {
|
||||||
|
dataMap, ok := data.Interface().(*gredis.Message)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// if dataMap. == "subscribe" {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
if dataMap.Channel == subscribeTopic {
|
||||||
|
cool.Logger.Debug(ctx, "执行函数", "payload", dataMap.Payload)
|
||||||
|
err := cool.RunFunc(ctx, dataMap.Payload)
|
||||||
|
if err != nil {
|
||||||
|
cool.Logger.Error(ctx, "执行函数失败", "payload", dataMap.Payload, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dataMap.Channel == "sun:join" {
|
||||||
|
|
||||||
|
fightmap.ADD(dataMap.Payload)
|
||||||
|
//universalClient, _ := g.Redis("cool").Client().(goredis.UniversalClient)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 清理资源,准备重连
|
||||||
|
_ = conn.Close(ctx) // 关闭当前连接
|
||||||
|
cool.Logger.Info(ctx, "Redis 订阅连接异常,准备重连", "retry_after", retryDelay)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenFight 完全对齐 ListenFunc 写法,修复收不到消息问题。
|
||||||
|
// 注意:PubSub 连接只负责订阅和接收,避免在同一连接上并发 PING。
|
||||||
|
func ListenFight(ctx g.Ctx) {
|
||||||
|
if !cool.IsRedisMode {
|
||||||
|
panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义常量配置(对齐 ListenFunc 风格)
|
||||||
|
const (
|
||||||
|
retryDelay = 10 * time.Second // 连接失败重试间隔
|
||||||
|
)
|
||||||
|
|
||||||
|
// 提前拼接订阅主题(避免重复拼接,便于日志打印)
|
||||||
|
serverID := cool.Config.ServerInfo.GetID()
|
||||||
|
startTopic := "sun:start:" + serverID
|
||||||
|
sendPackTopic := "sendpack:" + serverID
|
||||||
|
pvpServerTopic := pvpwire.ServerTopic(gconv.Uint32(serverID))
|
||||||
|
pvpCoordinatorTopic := pvpwire.CoordinatorTopicPrefix
|
||||||
|
|
||||||
|
// 外层循环:负责连接断开后的整体重连
|
||||||
|
for {
|
||||||
|
|
||||||
|
// 检查上下文是否已取消,优雅退出(对齐 ListenFunc)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
cool.Logger.Info(ctx, "ListenFight 上下文已取消,退出监听")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 建立 Redis 连接(完全对齐 ListenFunc)
|
||||||
|
conn, err := cool.Redis.Conn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
cool.Logger.Error(ctx, "获取 Redis 连接失败", "error", err, "retry_after", retryDelay)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 订阅主题(对齐 ListenFunc 的错误处理,替换 panic 为优雅重连)
|
||||||
|
subscribeTopics := []string{startTopic, pvpServerTopic}
|
||||||
|
if cool.Config.GameOnlineID == pvp.CoordinatorOnlineID {
|
||||||
|
subscribeTopics = append(subscribeTopics, pvpCoordinatorTopic)
|
||||||
|
}
|
||||||
|
subscribeFailed := false
|
||||||
|
for _, topic := range subscribeTopics {
|
||||||
|
_, err = conn.Do(ctx, "subscribe", topic)
|
||||||
|
if err != nil {
|
||||||
|
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", topic, "error", err)
|
||||||
|
_ = conn.Close(ctx)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
subscribeFailed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", topic)
|
||||||
|
}
|
||||||
|
if subscribeFailed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// // 订阅 sun:sendpack:服务器ID
|
||||||
|
// _, err = conn.Do(ctx, "subscribe", sendPackTopic)
|
||||||
|
// if err != nil {
|
||||||
|
// cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", sendPackTopic, "error", err)
|
||||||
|
// heartbeatCancel() // 关闭心跳协程
|
||||||
|
// _ = conn.Close(ctx)
|
||||||
|
// time.Sleep(retryDelay)
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", sendPackTopic)
|
||||||
|
|
||||||
|
// 打印监听提示(保留原有日志)
|
||||||
|
fmt.Println("监听战斗", startTopic)
|
||||||
|
|
||||||
|
// 3. 循环接收消息(完全对齐 ListenFunc 逻辑)
|
||||||
|
connError := false
|
||||||
|
for !connError {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
connError = true
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收消息(和 ListenFunc 保持一致的 Receive 用法)
|
||||||
|
data, err := conn.Receive(ctx)
|
||||||
|
if err != nil {
|
||||||
|
cool.Logger.Error(ctx, "Redis PubSub Receive 失败", "error", err)
|
||||||
|
connError = true // 标记连接错误,触发重连
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理消息(完全对齐 ListenFunc 的解析逻辑)
|
||||||
|
if data != nil {
|
||||||
|
dataMap, ok := data.Interface().(*gredis.Message)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 sun:start:服务器ID 消息
|
||||||
|
if dataMap.Channel == startTopic {
|
||||||
|
fmt.Println("战斗开始", dataMap.Payload)
|
||||||
|
// universalClient, _ := g.Redis("cool").Client().(goredis.UniversalClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dataMap.Channel == pvpServerTopic || dataMap.Channel == pvpCoordinatorTopic {
|
||||||
|
pvp.HandleRedisMessage(dataMap.Channel, dataMap.Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 【可选】处理 sun:sendpack:服务器ID 消息(如果需要)
|
||||||
|
if dataMap.Channel == sendPackTopic {
|
||||||
|
fmt.Println("收到战斗包", dataMap.Payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 清理资源,准备重连(完全对齐 ListenFunc)
|
||||||
|
_ = conn.Close(ctx) // 关闭当前连接
|
||||||
|
cool.Logger.Info(ctx, "Redis 战斗订阅连接异常,准备重连", "retry_after", retryDelay)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
163
common/rpc/pvp_match.go
Normal file
163
common/rpc/pvp_match.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/cool"
|
||||||
|
"blazing/logic/service/fight/pvpwire"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pvpMatchQueueTTL = 12 * time.Second
|
||||||
|
pvpMatchBanPickSecond = 45
|
||||||
|
)
|
||||||
|
|
||||||
|
type PVPMatchJoinPayload struct {
|
||||||
|
RuntimeServerID uint32 `json:"runtimeServerId"`
|
||||||
|
UserID uint32 `json:"userId"`
|
||||||
|
Nick string `json:"nick"`
|
||||||
|
FightMode uint32 `json:"fightMode"`
|
||||||
|
Status uint32 `json:"status"`
|
||||||
|
CatchTimes []uint32 `json:"catchTimes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pvpMatchCoordinator struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
queues map[uint32][]pvpwire.QueuePlayerSnapshot
|
||||||
|
lastSeen map[uint32]time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPVPMatchCoordinator = &pvpMatchCoordinator{
|
||||||
|
queues: make(map[uint32][]pvpwire.QueuePlayerSnapshot),
|
||||||
|
lastSeen: make(map[uint32]time.Time),
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultPVPMatchCoordinator() *pvpMatchCoordinator {
|
||||||
|
return defaultPVPMatchCoordinator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *pvpMatchCoordinator) JoinOrUpdate(payload PVPMatchJoinPayload) error {
|
||||||
|
if payload.UserID == 0 || payload.RuntimeServerID == 0 || payload.FightMode == 0 {
|
||||||
|
return fmt.Errorf("invalid pvp match payload: uid=%d server=%d mode=%d", payload.UserID, payload.RuntimeServerID, payload.FightMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
player := pvpwire.QueuePlayerSnapshot{
|
||||||
|
RuntimeServerID: payload.RuntimeServerID,
|
||||||
|
UserID: payload.UserID,
|
||||||
|
Nick: payload.Nick,
|
||||||
|
FightMode: payload.FightMode,
|
||||||
|
Status: payload.Status,
|
||||||
|
JoinedAtUnix: now.Unix(),
|
||||||
|
CatchTimes: append([]uint32(nil), payload.CatchTimes...),
|
||||||
|
}
|
||||||
|
|
||||||
|
var match *pvpwire.MatchFoundPayload
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.pruneExpiredLocked(now)
|
||||||
|
m.removeUserLocked(payload.UserID)
|
||||||
|
m.lastSeen[payload.UserID] = now
|
||||||
|
|
||||||
|
queue := m.queues[payload.FightMode]
|
||||||
|
if len(queue) > 0 {
|
||||||
|
host := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
m.queues[payload.FightMode] = queue
|
||||||
|
delete(m.lastSeen, host.UserID)
|
||||||
|
delete(m.lastSeen, payload.UserID)
|
||||||
|
|
||||||
|
result := pvpwire.MatchFoundPayload{
|
||||||
|
SessionID: buildPVPMatchSessionID(host.UserID, payload.UserID),
|
||||||
|
Stage: pvpwire.StageBanPick,
|
||||||
|
Host: host,
|
||||||
|
Guest: player,
|
||||||
|
BanPickTimeout: pvpMatchBanPickSecond,
|
||||||
|
}
|
||||||
|
match = &result
|
||||||
|
} else {
|
||||||
|
m.queues[payload.FightMode] = append(queue, player)
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
if match == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := publishPVPMatchMessage(pvpwire.ServerTopic(match.Host.RuntimeServerID), pvpwire.MessageTypeMatchFound, *match); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if match.Guest.RuntimeServerID != match.Host.RuntimeServerID {
|
||||||
|
if err := publishPVPMatchMessage(pvpwire.ServerTopic(match.Guest.RuntimeServerID), pvpwire.MessageTypeMatchFound, *match); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *pvpMatchCoordinator) Cancel(userID uint32) {
|
||||||
|
if userID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
delete(m.lastSeen, userID)
|
||||||
|
m.removeUserLocked(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *pvpMatchCoordinator) pruneExpiredLocked(now time.Time) {
|
||||||
|
for mode, queue := range m.queues {
|
||||||
|
next := make([]pvpwire.QueuePlayerSnapshot, 0, len(queue))
|
||||||
|
for _, queued := range queue {
|
||||||
|
last := m.lastSeen[queued.UserID]
|
||||||
|
if last.IsZero() || now.Sub(last) > pvpMatchQueueTTL {
|
||||||
|
delete(m.lastSeen, queued.UserID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next = append(next, queued)
|
||||||
|
}
|
||||||
|
m.queues[mode] = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *pvpMatchCoordinator) removeUserLocked(userID uint32) {
|
||||||
|
for mode, queue := range m.queues {
|
||||||
|
next := make([]pvpwire.QueuePlayerSnapshot, 0, len(queue))
|
||||||
|
for _, queued := range queue {
|
||||||
|
if queued.UserID == userID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next = append(next, queued)
|
||||||
|
}
|
||||||
|
m.queues[mode] = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishPVPMatchMessage(topic, msgType string, body any) error {
|
||||||
|
payload, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
envelope, err := json.Marshal(pvpwire.Envelope{
|
||||||
|
Type: msgType,
|
||||||
|
Body: payload,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn, err := cool.Redis.Conn(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close(context.Background())
|
||||||
|
|
||||||
|
_, err = conn.Do(context.Background(), "publish", topic, envelope)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPVPMatchSessionID(hostUserID, guestUserID uint32) string {
|
||||||
|
return fmt.Sprintf("xsvr-%d-%d-%d", hostUserID, guestUserID, time.Now().UnixNano())
|
||||||
|
}
|
||||||
@@ -1,109 +1,166 @@
|
|||||||
package rpc
|
package rpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"blazing/common/data/share"
|
"blazing/common/data/share"
|
||||||
"blazing/cool"
|
"blazing/cool"
|
||||||
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
config "blazing/modules/config/service"
|
|
||||||
|
config "blazing/modules/config/service"
|
||||||
"github.com/filecoin-project/go-jsonrpc"
|
|
||||||
"github.com/gogf/gf/v2/util/gconv"
|
"github.com/filecoin-project/go-jsonrpc"
|
||||||
)
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
// Define the server handler
|
|
||||||
type ServerHandler struct{}
|
// Define the server handler
|
||||||
|
type ServerHandler struct{}
|
||||||
// 实现踢人
|
|
||||||
func (*ServerHandler) Kick(_ context.Context, userid uint32) error {
|
const kickForwardTimeout = 3 * time.Second
|
||||||
|
|
||||||
useid1, err := share.ShareManager.GetUserOnline(userid)
|
// 实现踢人
|
||||||
|
func (*ServerHandler) Kick(_ context.Context, userid uint32) error {
|
||||||
if err != nil {
|
useid1, err := share.ShareManager.GetUserOnline(userid)
|
||||||
return fmt.Errorf("user not found", err)
|
if err != nil || useid1 == 0 {
|
||||||
}
|
// 请求到达时用户已离线,直接视为成功
|
||||||
|
return nil
|
||||||
cl, ok := cool.GetClient(useid1)
|
}
|
||||||
if ok {
|
|
||||||
err := cl.KickPerson(userid) //实现指定服务器踢人
|
cl, ok := cool.GetClientOnly(useid1)
|
||||||
if err != nil {
|
if !ok || cl == nil {
|
||||||
return fmt.Errorf("踢人失败", err)
|
// 目标服务器不在线,清理僵尸在线标记并视为成功
|
||||||
}
|
_ = share.ShareManager.DeleteUserOnline(userid)
|
||||||
}
|
cool.DeleteClientOnly(useid1)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册logic服务器
|
resultCh := make(chan error, 1)
|
||||||
func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint16) error {
|
go func() {
|
||||||
fmt.Println("注册logic服务器", id, port)
|
resultCh <- cl.KickPerson(userid) // 实现指定服务器踢人
|
||||||
|
}()
|
||||||
//TODO 待修复滚动更新可能导致的玩家可以同时在旧服务器和新服务器同时在线的bug
|
|
||||||
revClient, ok := jsonrpc.ExtractReverseClient[cool.ClientHandler](ctx)
|
select {
|
||||||
if !ok {
|
case callErr := <-resultCh:
|
||||||
return fmt.Errorf("no reverse client")
|
if callErr == nil {
|
||||||
}
|
return nil
|
||||||
t := config.NewServerService().GetServerID((id))
|
}
|
||||||
|
|
||||||
aa, ok := cool.GetClient(t.Port)
|
// 调用失败后兜底:用户若已离线/切服/目标服不在线都算成功
|
||||||
if ok && aa != nil { //如果已经存在且这个端口已经被存过
|
useid2, err2 := share.ShareManager.GetUserOnline(userid)
|
||||||
aa.QuitSelf(0)
|
if err2 != nil || useid2 == 0 || useid2 != useid1 {
|
||||||
}
|
return nil
|
||||||
cool.AddClient(port, &revClient)
|
}
|
||||||
|
if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil {
|
||||||
//Refurh()
|
_ = share.ShareManager.DeleteUserOnline(userid)
|
||||||
return nil
|
cool.DeleteClientOnly(useid2)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CServer() *jsonrpc.RPCServer {
|
// 仍在线则返回失败,不按成功处理
|
||||||
// create a new server instance
|
return callErr
|
||||||
rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler](""))
|
case <-time.After(kickForwardTimeout):
|
||||||
|
// 仅防止无限等待;超时不算成功
|
||||||
rpcServer.Register("", &ServerHandler{})
|
useid2, err2 := share.ShareManager.GetUserOnline(userid)
|
||||||
|
if err2 != nil || useid2 == 0 || useid2 != useid1 {
|
||||||
return rpcServer
|
return nil
|
||||||
|
}
|
||||||
}
|
if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil {
|
||||||
|
_ = share.ShareManager.DeleteUserOnline(userid)
|
||||||
var closer jsonrpc.ClientCloser
|
cool.DeleteClientOnly(useid2)
|
||||||
|
return nil
|
||||||
func StartClient(id, port uint16, callback any) *struct {
|
}
|
||||||
Kick func(uint32) error
|
|
||||||
|
return fmt.Errorf("kick timeout, user still online: uid=%d server=%d", userid, useid2)
|
||||||
RegisterLogic func(uint16, uint16) error
|
}
|
||||||
} {
|
}
|
||||||
|
|
||||||
var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc"
|
// 注册logic服务器
|
||||||
//rpcaddr = "127.0.0.1"
|
func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error {
|
||||||
closer1, err := jsonrpc.NewMergeClient(context.Background(),
|
fmt.Println("注册logic服务器", id, port)
|
||||||
rpcaddr, "", []interface{}{
|
|
||||||
&RPCClient,
|
//TODO 待修复滚动更新可能导致的玩家可以同时在旧服务器和新服务器同时在线的bug
|
||||||
}, nil, jsonrpc.WithClientHandler("", callback),
|
revClient, ok := jsonrpc.ExtractReverseClient[cool.ClientHandler](ctx)
|
||||||
jsonrpc.WithReconnFun(func() { RPCClient.RegisterLogic(id, port) }),
|
if !ok {
|
||||||
)
|
return fmt.Errorf("no reverse client")
|
||||||
if err != nil {
|
}
|
||||||
log.Fatalf("Failed to create client: %v", err)
|
t := config.NewServerService().GetServerID((id))
|
||||||
}
|
|
||||||
|
aa, ok := cool.GetClient(t.OnlineID, t.Port)
|
||||||
//if port != 0 { //注册logic
|
if ok && aa != nil { //如果已经存在且这个端口已经被存过
|
||||||
defer RPCClient.RegisterLogic(id, port)
|
aa.QuitSelf(0)
|
||||||
|
}
|
||||||
//}
|
cool.AddClient(100000*id+port, &revClient)
|
||||||
|
|
||||||
closer = closer1
|
//Refurh()
|
||||||
|
return nil
|
||||||
return &RPCClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup RPCClient with reverse call handler
|
func (*ServerHandler) MatchJoinOrUpdate(_ context.Context, payload PVPMatchJoinPayload) error {
|
||||||
var RPCClient struct {
|
return DefaultPVPMatchCoordinator().JoinOrUpdate(payload)
|
||||||
Kick func(uint32) error //踢人
|
}
|
||||||
|
|
||||||
RegisterLogic func(uint16, uint16) error
|
func (*ServerHandler) MatchCancel(_ context.Context, userID uint32) error {
|
||||||
|
DefaultPVPMatchCoordinator().Cancel(userID)
|
||||||
// UserLogin func(int32, int32) error //用户登录事件
|
return nil
|
||||||
// UserLogout func(int32, int32) error //用户登出事件
|
}
|
||||||
}
|
|
||||||
|
func CServer() *jsonrpc.RPCServer {
|
||||||
|
// create a new server instance
|
||||||
|
rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler](""))
|
||||||
|
|
||||||
|
rpcServer.Register("", &ServerHandler{})
|
||||||
|
|
||||||
|
return rpcServer
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var closer jsonrpc.ClientCloser
|
||||||
|
|
||||||
|
func StartClient(id, port uint32, callback any) *struct {
|
||||||
|
Kick func(uint32) error
|
||||||
|
|
||||||
|
RegisterLogic func(uint32, uint32) error
|
||||||
|
|
||||||
|
MatchJoinOrUpdate func(PVPMatchJoinPayload) error
|
||||||
|
|
||||||
|
MatchCancel func(uint32) error
|
||||||
|
} {
|
||||||
|
//cool.Config.File.Domain = "127.0.0.1"
|
||||||
|
var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc"
|
||||||
|
|
||||||
|
closer1, err := jsonrpc.NewMergeClient(context.Background(),
|
||||||
|
rpcaddr, "", []interface{}{
|
||||||
|
&RPCClient,
|
||||||
|
}, nil, jsonrpc.WithClientHandler("", callback),
|
||||||
|
jsonrpc.WithReconnFun(func() { RPCClient.RegisterLogic(id, port) }),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//if port != 0 { //注册logic
|
||||||
|
defer RPCClient.RegisterLogic(id, port)
|
||||||
|
|
||||||
|
//}
|
||||||
|
|
||||||
|
closer = closer1
|
||||||
|
|
||||||
|
return &RPCClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup RPCClient with reverse call handler
|
||||||
|
var RPCClient struct {
|
||||||
|
Kick func(uint32) error //踢人
|
||||||
|
|
||||||
|
RegisterLogic func(uint32, uint32) error
|
||||||
|
|
||||||
|
MatchJoinOrUpdate func(PVPMatchJoinPayload) error
|
||||||
|
|
||||||
|
MatchCancel func(uint32) error
|
||||||
|
|
||||||
|
// UserLogin func(int32, int32) error //用户登录事件
|
||||||
|
// UserLogout func(int32, int32) error //用户登出事件
|
||||||
|
}
|
||||||
|
|||||||
76
common/rpc/user.go
Normal file
76
common/rpc/user.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/data/share"
|
||||||
|
"blazing/cool"
|
||||||
|
"blazing/logic/service/common"
|
||||||
|
"blazing/logic/service/fight/info"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
"blazing/modules/player/service"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
"github.com/liwnn/zset"
|
||||||
|
csmap "github.com/mhmtszr/concurrent-swiss-map"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RPCfight struct {
|
||||||
|
fightmap *csmap.CsMap[int, common.FightI]
|
||||||
|
zs *zset.ZSet[uint32, *model.PVP]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RPCfight) join(pvp info.RPCFightinfo) {
|
||||||
|
|
||||||
|
ret := service.NewPVPService(pvp.PlayerID).Get(pvp.PlayerID)
|
||||||
|
ret.RankInfo.LastMatchTime = gtime.Now()
|
||||||
|
|
||||||
|
r.zs.Add(pvp.PlayerID, ret)
|
||||||
|
if r.zs.Length() > 1 {
|
||||||
|
u, _ := r.zs.FindNext(func(i *model.PVP) bool { return i.RankInfo.Score >= ret.RankInfo.Score })
|
||||||
|
|
||||||
|
diff := u.RankInfo.Score - ret.RankInfo.Score
|
||||||
|
// 等待越久,允许区间越大
|
||||||
|
wait := time.Now().Sub(u.RankInfo.LastMatchTime.Time).Seconds()
|
||||||
|
maxAllow := 100 + int(wait)*10
|
||||||
|
if diff < maxAllow {
|
||||||
|
//找到上一个,如果区间分数少于一定,
|
||||||
|
//直接进行匹配
|
||||||
|
useid1, _ := share.ShareManager.GetUserOnline(u.PlayerID)
|
||||||
|
cool.RedisDo(context.TODO(), "sun:start:"+gconv.String(useid1), info.RPCFightStartinfo{
|
||||||
|
Serverid: int(useid1),
|
||||||
|
PlayerID: u.PlayerID,
|
||||||
|
Mode: pvp.Mode,
|
||||||
|
Status: pvp.Status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RPCfight) ADD(s string) {
|
||||||
|
println("收到sun:join", s)
|
||||||
|
var pvp []info.RPCFightinfo
|
||||||
|
|
||||||
|
json.Unmarshal([]byte(s), &pvp)
|
||||||
|
if pvp[0].Type == 1 {
|
||||||
|
r.join(pvp[0])
|
||||||
|
} else { //==0 退出
|
||||||
|
r.cancel(pvp[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func (r *RPCfight) cancel(pvp info.RPCFightinfo) {
|
||||||
|
r.zs.Remove(pvp.PlayerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
///定义map,存储用户对战斗容器的映射,便于外部传入时候进行直接操作
|
||||||
|
|
||||||
|
var fightmap = RPCfight{
|
||||||
|
fightmap: csmap.New[int, common.FightI](),
|
||||||
|
zs: zset.New[uint32, *model.PVP](func(a, b *model.PVP) bool {
|
||||||
|
return a.Less(b)
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -1,211 +1,266 @@
|
|||||||
package socket
|
package socket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"blazing/common/socket/codec"
|
||||||
"fmt"
|
"blazing/cool"
|
||||||
"log"
|
"blazing/logic/service/player"
|
||||||
"os"
|
"blazing/modules/config/service"
|
||||||
"sync/atomic"
|
"bytes"
|
||||||
"time"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
"blazing/common/socket/codec"
|
"errors"
|
||||||
"blazing/cool"
|
"log"
|
||||||
"blazing/logic/service/player"
|
"os"
|
||||||
|
"sync/atomic"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"time"
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
|
||||||
"github.com/panjf2000/gnet/v2"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
)
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
"github.com/panjf2000/gnet/v2"
|
||||||
func (s *Server) Boot() error {
|
)
|
||||||
// go s.bootws()
|
|
||||||
err := gnet.Run(s, s.network+"://"+s.addr,
|
const (
|
||||||
gnet.WithMulticore(true),
|
minPacketLen = 17
|
||||||
gnet.WithTicker(true),
|
maxPacketLen = 10 * 1024
|
||||||
|
)
|
||||||
// gnet.WithReusePort(true),
|
|
||||||
// gnet.WithReuseAddr(true),
|
func (s *Server) Boot(serverid, port uint32) error {
|
||||||
gnet.WithSocketRecvBuffer(s.bufferSize))
|
// go s.bootws()
|
||||||
if err != nil {
|
s.serverid = serverid
|
||||||
panic(err)
|
s.port = port
|
||||||
}
|
|
||||||
|
err := gnet.Run(s, s.network+"://"+s.addr,
|
||||||
return nil
|
gnet.WithMulticore(true),
|
||||||
}
|
gnet.WithTicker(true),
|
||||||
|
|
||||||
func (s *Server) Stop() error {
|
// 其他调优配置↓
|
||||||
_ = s.eng.Stop(context.Background())
|
gnet.WithTCPNoDelay(gnet.TCPNoDelay), // 禁用Nagle算法(降低延迟,适合小数据包场景)
|
||||||
s.workerPool.Release()
|
//gnet.WithReusePort(true), // 开启SO_REUSEPORT(多核下提升并发)
|
||||||
|
//gnet.WithReadBufferCap(1024*64), // 读缓冲区64KB(根据业务调整,默认太小)
|
||||||
return nil
|
// gnet.WithWriteBufferCap(1024*64), // 写缓冲区64KB
|
||||||
}
|
|
||||||
|
//gnet.WithLockOSThread(true), // 绑定goroutine到OS线程(减少上下文切换)
|
||||||
func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
|
)
|
||||||
defer func() {
|
if err != nil {
|
||||||
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
panic(err)
|
||||||
// 1. 打印错误信息
|
}
|
||||||
|
|
||||||
fmt.Println(context.TODO(), "panic 错误:", err)
|
return nil
|
||||||
|
}
|
||||||
}
|
|
||||||
}()
|
func (s *Server) Stop() error {
|
||||||
// 识别 RST 导致的连接中断(错误信息含 "connection reset")
|
_ = s.eng.Stop(context.Background())
|
||||||
// if err != nil && (strings.Contains(err.Error(), "connection reset") || strings.Contains(err.Error(), "reset by peer")) {
|
s.workerPool.Release()
|
||||||
// remoteIP := c.RemoteAddr().(*net.TCPAddr).IP.String()
|
|
||||||
|
return nil
|
||||||
// log.Printf("RST 攻击检测: 来源 %s, 累计攻击次数 %d", remoteIP)
|
}
|
||||||
|
|
||||||
// // 防护逻辑:临时封禁异常 IP(可扩展为 IP 黑名单)
|
func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
|
||||||
// // go s.tempBlockIP(remoteIP, 5*time.Minute)
|
defer func() {
|
||||||
// }
|
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
||||||
//fmt.Println(err, c.RemoteAddr().String(), "断开连接")
|
if t, ok := c.Context().(*player.ClientData); ok {
|
||||||
atomic.AddInt64(&s.connected, -1)
|
if t.Player != nil {
|
||||||
|
if t.Player.Info != nil {
|
||||||
//logging.Infof("conn[%v] disconnected", c.RemoteAddr().String())
|
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
|
||||||
v, _ := c.Context().(*player.ClientData)
|
go t.Player.SaveOnDisconnect()
|
||||||
v.LF.Close()
|
}
|
||||||
if v.Player != nil {
|
|
||||||
v.Player.Save() //保存玩家数据
|
}
|
||||||
|
} else {
|
||||||
}
|
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, err)
|
||||||
|
}
|
||||||
//}
|
}
|
||||||
//关闭连接
|
}()
|
||||||
return
|
|
||||||
}
|
atomic.AddInt64(&cool.Connected, -1)
|
||||||
func (s *Server) OnTick() (delay time.Duration, action gnet.Action) {
|
|
||||||
g.Log().Async().Info(context.Background(), gtime.Now().ISO8601(), cool.Config.ServerInfo.OnlineID, "链接数", atomic.LoadInt64(&s.connected))
|
v, _ := c.Context().(*player.ClientData)
|
||||||
if s.quit && atomic.LoadInt64(&s.connected) == 0 {
|
if v != nil {
|
||||||
//执行正常退出逻辑
|
v.Close()
|
||||||
os.Exit(0)
|
if v.Player != nil {
|
||||||
}
|
v.Player.Save() //保存玩家数据
|
||||||
return 30 * time.Second, gnet.None
|
}
|
||||||
}
|
}
|
||||||
func (s *Server) OnBoot(eng gnet.Engine) gnet.Action {
|
return
|
||||||
s.eng = eng
|
}
|
||||||
|
|
||||||
return gnet.None
|
func (s *Server) OnTick() (delay time.Duration, action gnet.Action) {
|
||||||
}
|
g.Log().Async().Info(context.Background(), gtime.Now().ISO8601(), "服务器ID", cool.Config.ServerInfo.OnlineID, "链接数", atomic.LoadInt64(&cool.Connected))
|
||||||
|
if s.quit && atomic.LoadInt64(&cool.Connected) == 0 {
|
||||||
func (s *Server) OnOpen(conn gnet.Conn) (out []byte, action gnet.Action) {
|
os.Exit(0)
|
||||||
if s.network != "tcp" {
|
}
|
||||||
return nil, gnet.Close
|
return 30 * time.Second, gnet.None
|
||||||
}
|
}
|
||||||
|
|
||||||
if conn.Context() == nil {
|
func (s *Server) OnBoot(eng gnet.Engine) gnet.Action {
|
||||||
conn.SetContext(player.NewClientData(conn)) //注入data
|
s.eng = eng
|
||||||
}
|
service.NewServerService().SetServerID(s.serverid, s.port)
|
||||||
|
return gnet.None
|
||||||
atomic.AddInt64(&s.connected, 1)
|
}
|
||||||
|
|
||||||
return nil, gnet.None
|
func (s *Server) OnOpen(conn gnet.Conn) (out []byte, action gnet.Action) {
|
||||||
}
|
if s.network != "tcp" {
|
||||||
|
return nil, gnet.Close
|
||||||
func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
|
}
|
||||||
defer func() {
|
if conn.Context() == nil {
|
||||||
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
conn.SetContext(player.NewClientData(conn))
|
||||||
// 1. 打印错误信息
|
}
|
||||||
|
atomic.AddInt64(&cool.Connected, 1)
|
||||||
cool.Logger.Error(context.TODO(), "panic 错误:", err)
|
return nil, gnet.None
|
||||||
|
}
|
||||||
}
|
|
||||||
}()
|
func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
|
||||||
|
defer func() {
|
||||||
ws := c.Context().(*player.ClientData).Wsmsg
|
if err := recover(); err != nil {
|
||||||
if ws.Tcp { //升级失败时候防止缓冲区溢出
|
if t, ok := c.Context().(*player.ClientData); ok {
|
||||||
return s.handleTCP(c)
|
if t.Player != nil && t.Player.Info != nil {
|
||||||
|
cool.Logger.Error(context.TODO(), "OnTraffic 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
|
||||||
}
|
t.Player.Service.Info.Save(*t.Player.Info)
|
||||||
|
}
|
||||||
tt, len1 := ws.ReadBufferBytes(c)
|
}
|
||||||
if tt == gnet.Close {
|
}
|
||||||
|
}()
|
||||||
return gnet.Close
|
|
||||||
}
|
client := c.Context().(*player.ClientData)
|
||||||
|
if s.discorse && !client.IsCrossDomainChecked() {
|
||||||
ok, action := ws.Upgrade(c)
|
handled, ready, action := handle(c)
|
||||||
if action != gnet.None { //连接断开
|
if action != gnet.None {
|
||||||
return action
|
return action
|
||||||
}
|
}
|
||||||
if !ok { //升级失败,说明是tcp连接
|
if handled {
|
||||||
ws.Tcp = true
|
client.MarkCrossDomainChecked()
|
||||||
|
return gnet.None
|
||||||
return s.handleTCP(c)
|
}
|
||||||
|
if !ready {
|
||||||
}
|
return gnet.None
|
||||||
// fmt.Println(ws.Buf.Bytes())
|
}
|
||||||
c.Discard(len1)
|
client.MarkCrossDomainChecked()
|
||||||
|
}
|
||||||
messages, err := ws.Decode(c)
|
|
||||||
if err != nil {
|
ws := client.Wsmsg
|
||||||
return gnet.Close
|
if ws.Tcp {
|
||||||
}
|
return s.handleTCP(c)
|
||||||
if messages == nil {
|
}
|
||||||
return
|
|
||||||
}
|
readAction, inboundLen := ws.ReadBufferBytes(c)
|
||||||
|
if readAction == gnet.Close {
|
||||||
t := c.Context().(*player.ClientData)
|
return gnet.Close
|
||||||
for _, msg := range messages {
|
}
|
||||||
t.LF.Producer().Write(msg.Payload)
|
|
||||||
//t.OnEvent(msg.Payload)
|
state, action := ws.Upgrade(c)
|
||||||
}
|
if action != gnet.None {
|
||||||
|
return action
|
||||||
return gnet.None
|
}
|
||||||
}
|
if state == player.UpgradeNeedMoreData {
|
||||||
|
return gnet.None
|
||||||
func (s *Server) handleTCP(conn gnet.Conn) (action gnet.Action) {
|
}
|
||||||
|
if state == player.UpgradeUseTCP {
|
||||||
conn.Context().(*player.ClientData).IsCrossDomain.Do(func() { //跨域检测
|
return s.handleTCP(c)
|
||||||
handle(conn)
|
}
|
||||||
})
|
|
||||||
|
if inboundLen > 0 {
|
||||||
data, err := s.codec.Decode(conn)
|
if _, err := c.Discard(inboundLen); err != nil {
|
||||||
if err != nil {
|
return gnet.Close
|
||||||
if err == codec.ErrIncompletePacket {
|
}
|
||||||
|
ws.ResetInboundMirror()
|
||||||
return
|
}
|
||||||
}
|
|
||||||
return gnet.Close
|
messages, err := ws.Decode(c)
|
||||||
|
if err != nil {
|
||||||
}
|
return gnet.Close
|
||||||
|
}
|
||||||
if t, ok := conn.Context().(*player.ClientData); ok {
|
if messages == nil {
|
||||||
t.LF.Producer().Write(data)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if conn.InboundBuffered() > 0 {
|
for _, msg := range messages {
|
||||||
if err := conn.Wake(nil); err != nil { // wake up the connection manually to avoid missing the leftover data
|
if !s.onevent(c, msg.Payload) {
|
||||||
|
return gnet.Close
|
||||||
return gnet.Close
|
}
|
||||||
}
|
}
|
||||||
}
|
return gnet.None
|
||||||
return action
|
}
|
||||||
|
|
||||||
}
|
func (s *Server) handleTCP(conn gnet.Conn) (action gnet.Action) {
|
||||||
|
client := conn.Context().(*player.ClientData)
|
||||||
// CROSS_DOMAIN 定义跨域策略文件内容
|
if s.discorse && !client.IsCrossDomainChecked() {
|
||||||
const CROSS_DOMAIN = "<?xml version=\"1.0\"?><!DOCTYPE cross-domain-policy><cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>\x00"
|
handled, ready, action := handle(conn)
|
||||||
|
if action != gnet.None {
|
||||||
// TEXT 定义跨域请求的文本格式
|
return action
|
||||||
const TEXT = "<policy-file-request/>\x00"
|
}
|
||||||
|
if !ready {
|
||||||
func handle(c gnet.Conn) {
|
return gnet.None
|
||||||
|
}
|
||||||
// 读取数据并检查是否为跨域请求
|
if handled {
|
||||||
data, err := c.Peek(len(TEXT))
|
client.MarkCrossDomainChecked()
|
||||||
if err != nil {
|
return gnet.None
|
||||||
log.Printf("Error reading cross-domain request: %v", err)
|
}
|
||||||
return
|
client.MarkCrossDomainChecked()
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(data) == TEXT { //判断是否是跨域请求
|
body, err := s.codec.Decode(conn)
|
||||||
log.Printf("Received cross-domain request from %s", c.RemoteAddr())
|
if err != nil {
|
||||||
// 处理跨域请求
|
if errors.Is(err, codec.ErrIncompletePacket) {
|
||||||
c.Write([]byte(CROSS_DOMAIN))
|
return gnet.None
|
||||||
c.Discard(len(TEXT))
|
}
|
||||||
|
return gnet.Close
|
||||||
return
|
}
|
||||||
}
|
if !s.onevent(conn, body) {
|
||||||
|
return gnet.Close
|
||||||
//return
|
}
|
||||||
}
|
if conn.InboundBuffered() > 0 {
|
||||||
|
if err := conn.Wake(nil); err != nil {
|
||||||
|
return gnet.Close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
const CROSS_DOMAIN = "<?xml version=\"1.0\"?><!DOCTYPE cross-domain-policy><cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>\x00"
|
||||||
|
const TEXT = "<policy-file-request/>\x00"
|
||||||
|
|
||||||
|
func handle(c gnet.Conn) (handled bool, ready bool, action gnet.Action) {
|
||||||
|
probeLen := c.InboundBuffered()
|
||||||
|
if probeLen == 0 {
|
||||||
|
return false, false, gnet.None
|
||||||
|
}
|
||||||
|
if probeLen > len(TEXT) {
|
||||||
|
probeLen = len(TEXT)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := c.Peek(probeLen)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading cross-domain request: %v", err)
|
||||||
|
return false, false, gnet.Close
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, []byte(TEXT[:probeLen])) {
|
||||||
|
return false, true, gnet.None
|
||||||
|
}
|
||||||
|
if probeLen < len(TEXT) {
|
||||||
|
return false, false, gnet.None
|
||||||
|
}
|
||||||
|
if _, err := c.Write([]byte(CROSS_DOMAIN)); err != nil {
|
||||||
|
return false, true, gnet.Close
|
||||||
|
}
|
||||||
|
if _, err := c.Discard(len(TEXT)); err != nil {
|
||||||
|
return false, true, gnet.Close
|
||||||
|
}
|
||||||
|
return true, true, gnet.None
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) onevent(c gnet.Conn, v []byte) bool {
|
||||||
|
if !isValidPacket(v) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if t, ok := c.Context().(*player.ClientData); ok {
|
||||||
|
t.PushEvent(v, s.workerPool.Submit)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidPacket(v []byte) bool {
|
||||||
|
if len(v) < minPacketLen || len(v) > maxPacketLen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint32(v[0:4]) == uint32(len(v))
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ type Handler interface {
|
|||||||
}
|
}
|
||||||
type Server struct {
|
type Server struct {
|
||||||
gnet.BuiltinEventEngine
|
gnet.BuiltinEventEngine
|
||||||
eng gnet.Engine
|
eng gnet.Engine
|
||||||
addr string
|
addr string
|
||||||
connected int64
|
|
||||||
network string
|
network string
|
||||||
multicore bool
|
multicore bool
|
||||||
bufferSize int
|
bufferSize int
|
||||||
@@ -24,7 +24,9 @@ type Server struct {
|
|||||||
handler Handler
|
handler Handler
|
||||||
discorse bool
|
discorse bool
|
||||||
quit bool
|
quit bool
|
||||||
batchRead int
|
// batchRead int
|
||||||
|
serverid uint32
|
||||||
|
port uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option func(*Server)
|
type Option func(*Server)
|
||||||
@@ -36,9 +38,9 @@ func NewServer(options ...Option) *Server {
|
|||||||
// handler: handler.NewTomeeHandler(), //请求返回
|
// handler: handler.NewTomeeHandler(), //请求返回
|
||||||
codec: codec.NewTomeeSocketCodec(), //默认解码器 len+pack
|
codec: codec.NewTomeeSocketCodec(), //默认解码器 len+pack
|
||||||
workerPool: goroutine.Default(),
|
workerPool: goroutine.Default(),
|
||||||
bufferSize: 4096, //默认缓冲区大小
|
bufferSize: 40960, //默认缓冲区大小
|
||||||
multicore: true,
|
multicore: true,
|
||||||
batchRead: 8,
|
//batchRead: 8,
|
||||||
//discorse: true,
|
//discorse: true,
|
||||||
}
|
}
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package socket
|
|||||||
import (
|
import (
|
||||||
"blazing/logic/service/player"
|
"blazing/logic/service/player"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,9 +13,9 @@ type Broadcast struct {
|
|||||||
|
|
||||||
func (s *Server) Broadcast(t string) int {
|
func (s *Server) Broadcast(t string) int {
|
||||||
|
|
||||||
player.Mainplayer.Range(func(key uint32, value *player.Player) bool {
|
player.Mainplayer.Range(func(key uint32, value *player.ClientData) bool {
|
||||||
|
|
||||||
value.SendPackCmd(50003, &Broadcast{
|
value.Player.SendPackCmd(50003, &Broadcast{
|
||||||
Name: t,
|
Name: t,
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
@@ -41,30 +42,32 @@ func (s *Server) QuitSelf(a int) error {
|
|||||||
|
|
||||||
s.quit = true
|
s.quit = true
|
||||||
if a != 0 {
|
if a != 0 {
|
||||||
player.Mainplayer.Range(func(key uint32, value *player.Player) bool {
|
player.Mainplayer.Range(func(key uint32, value *player.ClientData) bool {
|
||||||
if value != nil {
|
if value != nil {
|
||||||
value.Kick()
|
value.Player.Kick(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
player.Mainplayer.Range(func(key uint32, value *player.Player) bool {
|
player.Mainplayer.Range(func(key uint32, value *player.ClientData) bool {
|
||||||
if value != nil {
|
if value != nil {
|
||||||
value.KickMessage()
|
value.Player.KickMessage()
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
<-time.After(10 * time.Minute)
|
<-time.After(10 * time.Minute)
|
||||||
player.Mainplayer.Range(func(key uint32, value *player.Player) bool {
|
player.Mainplayer.Range(func(key uint32, value *player.ClientData) bool {
|
||||||
if value != nil {
|
if value != nil {
|
||||||
value.Kick()
|
value.Player.Kick(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
os.Exit(0)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ package bitset32
|
|||||||
import "math/bits"
|
import "math/bits"
|
||||||
|
|
||||||
func popcntSlice(s []uint32) uint64 {
|
func popcntSlice(s []uint32) uint64 {
|
||||||
|
|
||||||
|
// int r = 0;
|
||||||
|
// while(n)
|
||||||
|
// {
|
||||||
|
// n &= (n - 1);
|
||||||
|
// ++r;
|
||||||
|
// }
|
||||||
var cnt int
|
var cnt int
|
||||||
for _, x := range s {
|
for _, x := range s {
|
||||||
cnt += bits.OnesCount32(x)
|
cnt += bits.OnesCount32(x)
|
||||||
|
|||||||
@@ -1,221 +1,195 @@
|
|||||||
package csmap
|
package csmap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"blazing/cool"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"github.com/mhmtszr/concurrent-swiss-map/maphash"
|
|
||||||
|
|
||||||
"github.com/mhmtszr/concurrent-swiss-map/swiss"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CsMap 基于官方 sync.Map 重构,完全兼容原有接口
|
||||||
type CsMap[K comparable, V any] struct {
|
type CsMap[K comparable, V any] struct {
|
||||||
hasher func(key K) uint64
|
inner sync.Map // 核心替换为官方 sync.Map
|
||||||
shards []shard[K, V]
|
|
||||||
shardCount uint64
|
|
||||||
size uint64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HashShardPair[K comparable, V any] struct {
|
// 以下配置方法保留(兼容原有调用方式,但内部无实际作用)
|
||||||
shard shard[K, V]
|
|
||||||
hash uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
type shard[K comparable, V any] struct {
|
|
||||||
items *swiss.Map[K, V]
|
|
||||||
*sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptFunc is a type that is used in New function for passing options.
|
|
||||||
type OptFunc[K comparable, V any] func(o *CsMap[K, V])
|
type OptFunc[K comparable, V any] func(o *CsMap[K, V])
|
||||||
|
|
||||||
// New function creates *CsMap[K, V].
|
// New 创建基于 sync.Map 的并发安全 Map,兼容原有配置参数(参数无实际作用)
|
||||||
func New[K comparable, V any](options ...OptFunc[K, V]) *CsMap[K, V] {
|
func New[K comparable, V any](options ...OptFunc[K, V]) *CsMap[K, V] {
|
||||||
m := CsMap[K, V]{
|
m := &CsMap[K, V]{}
|
||||||
hasher: maphash.NewHasher[K]().Hash,
|
// 遍历配置项(兼容原有代码,无实际逻辑)
|
||||||
shardCount: 32,
|
|
||||||
}
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
option(&m)
|
option(m)
|
||||||
}
|
}
|
||||||
|
return m
|
||||||
m.shards = make([]shard[K, V], m.shardCount)
|
|
||||||
|
|
||||||
for i := 0; i < int(m.shardCount); i++ {
|
|
||||||
m.shards[i] = shard[K, V]{items: swiss.NewMap[K, V](uint32((m.size / m.shardCount) + 1)), RWMutex: &sync.RWMutex{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Create creates *CsMap.
|
// 保留原有配置方法(空实现,保证接口兼容)
|
||||||
// //
|
|
||||||
// // Deprecated: New function should be used instead.
|
|
||||||
// func Create[K comparable, V any](options ...func(options *CsMap[K, V])) *CsMap[K, V] {
|
|
||||||
// m := CsMap[K, V]{
|
|
||||||
// hasher: maphash.NewHasher[K]().Hash,
|
|
||||||
// shardCount: 32,
|
|
||||||
// }
|
|
||||||
// for _, option := range options {
|
|
||||||
// option(&m)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// m.shards = make([]shard[K, V], m.shardCount)
|
|
||||||
|
|
||||||
// for i := 0; i < int(m.shardCount); i++ {
|
|
||||||
// m.shards[i] = shard[K, V]{items: swiss.NewMap[K, V](uint32((m.size / m.shardCount) + 1)), RWMutex: &sync.RWMutex{}}
|
|
||||||
// }
|
|
||||||
// return &m
|
|
||||||
// }
|
|
||||||
|
|
||||||
func WithShardCount[K comparable, V any](count uint64) func(csMap *CsMap[K, V]) {
|
func WithShardCount[K comparable, V any](count uint64) func(csMap *CsMap[K, V]) {
|
||||||
return func(csMap *CsMap[K, V]) {
|
return func(csMap *CsMap[K, V]) {}
|
||||||
csMap.shardCount = count
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithCustomHasher[K comparable, V any](h func(key K) uint64) func(csMap *CsMap[K, V]) {
|
func WithCustomHasher[K comparable, V any](h func(key K) uint64) func(csMap *CsMap[K, V]) {
|
||||||
return func(csMap *CsMap[K, V]) {
|
return func(csMap *CsMap[K, V]) {}
|
||||||
csMap.hasher = h
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithSize[K comparable, V any](size uint64) func(csMap *CsMap[K, V]) {
|
func WithSize[K comparable, V any](size uint64) func(csMap *CsMap[K, V]) {
|
||||||
return func(csMap *CsMap[K, V]) {
|
return func(csMap *CsMap[K, V]) {}
|
||||||
csMap.size = size
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CsMap[K, V]) getShard(key K) HashShardPair[K, V] {
|
// -------------------------- 核心操作方法(基于 sync.Map 实现) --------------------------
|
||||||
u := m.hasher(key)
|
|
||||||
return HashShardPair[K, V]{
|
|
||||||
hash: u,
|
|
||||||
shard: m.shards[u%m.shardCount],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Store 存储键值对,兼容原有接口
|
||||||
func (m *CsMap[K, V]) Store(key K, value V) {
|
func (m *CsMap[K, V]) Store(key K, value V) {
|
||||||
hashShardPair := m.getShard(key)
|
m.inner.Store(key, value)
|
||||||
shard := hashShardPair.shard
|
}
|
||||||
shard.Lock()
|
func (m *CsMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
|
||||||
shard.items.PutWithHash(key, value, hashShardPair.hash)
|
T, OK := m.inner.LoadOrStore(key, value)
|
||||||
shard.Unlock()
|
return T.(V), OK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete 删除指定键,返回是否删除成功
|
||||||
func (m *CsMap[K, V]) Delete(key K) bool {
|
func (m *CsMap[K, V]) Delete(key K) bool {
|
||||||
hashShardPair := m.getShard(key)
|
// sync.Map.Delete 无返回值,需先 Load 判断是否存在
|
||||||
shard := hashShardPair.shard
|
_, ok := m.inner.Load(key)
|
||||||
shard.Lock()
|
if ok {
|
||||||
defer shard.Unlock()
|
m.inner.Delete(key)
|
||||||
return shard.items.DeleteWithHash(key, hashShardPair.hash)
|
}
|
||||||
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteIf 满足条件时删除
|
||||||
func (m *CsMap[K, V]) DeleteIf(key K, condition func(value V) bool) bool {
|
func (m *CsMap[K, V]) DeleteIf(key K, condition func(value V) bool) bool {
|
||||||
hashShardPair := m.getShard(key)
|
// 先 Load 获取值,再判断条件
|
||||||
shard := hashShardPair.shard
|
val, ok := m.inner.Load(key)
|
||||||
shard.Lock()
|
if !ok {
|
||||||
defer shard.Unlock()
|
return false
|
||||||
value, ok := shard.items.GetWithHash(key, hashShardPair.hash)
|
}
|
||||||
if ok && condition(value) {
|
|
||||||
return shard.items.DeleteWithHash(key, hashShardPair.hash)
|
v, okCast := val.(V)
|
||||||
|
if !okCast {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if condition(v) {
|
||||||
|
m.inner.Delete(key)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load 获取指定键的值
|
||||||
func (m *CsMap[K, V]) Load(key K) (V, bool) {
|
func (m *CsMap[K, V]) Load(key K) (V, bool) {
|
||||||
hashShardPair := m.getShard(key)
|
var zero V
|
||||||
shard := hashShardPair.shard
|
val, ok := m.inner.Load(key)
|
||||||
shard.RLock()
|
if !ok {
|
||||||
defer shard.RUnlock()
|
return zero, false
|
||||||
return shard.items.GetWithHash(key, hashShardPair.hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *CsMap[K, V]) Has(key K) bool {
|
|
||||||
hashShardPair := m.getShard(key)
|
|
||||||
shard := hashShardPair.shard
|
|
||||||
shard.RLock()
|
|
||||||
defer shard.RUnlock()
|
|
||||||
return shard.items.HasWithHash(key, hashShardPair.hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *CsMap[K, V]) Clear() {
|
|
||||||
for i := range m.shards {
|
|
||||||
shard := m.shards[i]
|
|
||||||
|
|
||||||
shard.Lock()
|
|
||||||
shard.items.Clear()
|
|
||||||
shard.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 类型断言(保证类型安全)
|
||||||
|
v, okCast := val.(V)
|
||||||
|
if !okCast {
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
return v, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Has 判断键是否存在
|
||||||
|
func (m *CsMap[K, V]) Has(key K) bool {
|
||||||
|
_, ok := m.inner.Load(key)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 清空所有数据
|
||||||
|
func (m *CsMap[K, V]) Clear() {
|
||||||
|
// sync.Map 无直接 Clear 方法,通过 Range 遍历删除
|
||||||
|
m.inner.Range(func(key, value any) bool {
|
||||||
|
m.inner.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count 统计元素数量
|
||||||
func (m *CsMap[K, V]) Count() int {
|
func (m *CsMap[K, V]) Count() int {
|
||||||
count := 0
|
count := 0
|
||||||
for i := range m.shards {
|
m.inner.Range(func(key, value any) bool {
|
||||||
shard := m.shards[i]
|
count++
|
||||||
shard.RLock()
|
return true
|
||||||
count += shard.items.Count()
|
})
|
||||||
shard.RUnlock()
|
|
||||||
}
|
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetIfAbsent 仅当键不存在时设置值
|
||||||
func (m *CsMap[K, V]) SetIfAbsent(key K, value V) {
|
func (m *CsMap[K, V]) SetIfAbsent(key K, value V) {
|
||||||
hashShardPair := m.getShard(key)
|
m.inner.LoadOrStore(key, value)
|
||||||
shard := hashShardPair.shard
|
|
||||||
shard.Lock()
|
|
||||||
_, ok := shard.items.GetWithHash(key, hashShardPair.hash)
|
|
||||||
if !ok {
|
|
||||||
shard.items.PutWithHash(key, value, hashShardPair.hash)
|
|
||||||
}
|
|
||||||
shard.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CsMap[K, V]) SetIf(key K, conditionFn func(previousVale V, previousFound bool) (value V, set bool)) {
|
// SetIf 根据条件设置值
|
||||||
hashShardPair := m.getShard(key)
|
func (m *CsMap[K, V]) SetIf(key K, conditionFn func(previousValue V, previousFound bool) (value V, set bool)) {
|
||||||
shard := hashShardPair.shard
|
prevVal, found := m.inner.Load(key)
|
||||||
shard.Lock()
|
var prevV V
|
||||||
value, found := shard.items.GetWithHash(key, hashShardPair.hash)
|
if found {
|
||||||
value, ok := conditionFn(value, found)
|
prevV, _ = prevVal.(V)
|
||||||
if ok {
|
}
|
||||||
shard.items.PutWithHash(key, value, hashShardPair.hash)
|
|
||||||
|
// 执行条件函数
|
||||||
|
newVal, set := conditionFn(prevV, found)
|
||||||
|
if set {
|
||||||
|
m.inner.Store(key, newVal)
|
||||||
}
|
}
|
||||||
shard.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetIfPresent 仅当键存在时设置值
|
||||||
func (m *CsMap[K, V]) SetIfPresent(key K, value V) {
|
func (m *CsMap[K, V]) SetIfPresent(key K, value V) {
|
||||||
hashShardPair := m.getShard(key)
|
// 先判断是否存在,再设置
|
||||||
shard := hashShardPair.shard
|
if _, ok := m.inner.Load(key); ok {
|
||||||
shard.Lock()
|
m.inner.Store(key, value)
|
||||||
_, ok := shard.items.GetWithHash(key, hashShardPair.hash)
|
|
||||||
if ok {
|
|
||||||
shard.items.PutWithHash(key, value, hashShardPair.hash)
|
|
||||||
}
|
}
|
||||||
shard.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsEmpty 判断是否为空
|
||||||
func (m *CsMap[K, V]) IsEmpty() bool {
|
func (m *CsMap[K, V]) IsEmpty() bool {
|
||||||
return m.Count() == 0
|
return m.Count() == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tuple 保留原有结构体(兼容序列化逻辑)
|
||||||
type Tuple[K comparable, V any] struct {
|
type Tuple[K comparable, V any] struct {
|
||||||
Key K
|
Key K
|
||||||
Val V
|
Val V
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range If the callback function returns true iteration will stop.
|
// -------------------------- 关键修复:Range 方法(无锁阻塞风险) --------------------------
|
||||||
func (m *CsMap[K, V]) Range(f func(key K, value V) (stop bool)) {
|
func (m *CsMap[K, V]) Range(f func(key K, value V) (stop bool)) {
|
||||||
ch := make(chan Tuple[K, V], m.Count())
|
if f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
var stopFlag atomic.Bool
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
listenCompleted := m.listen(f, ch)
|
// 基于 sync.Map 的 Range 实现,无额外 goroutine/channel
|
||||||
m.produce(ctx, ch)
|
m.inner.Range(func(key, value any) bool {
|
||||||
listenCompleted.Wait()
|
// 检测终止标志
|
||||||
|
if stopFlag.Load() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型断言
|
||||||
|
k, okK := key.(K)
|
||||||
|
v, okV := value.(V)
|
||||||
|
if !okK || !okV {
|
||||||
|
return true // 类型不匹配时跳过,继续遍历
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行用户回调
|
||||||
|
if f(k, v) {
|
||||||
|
stopFlag.Store(true)
|
||||||
|
return false // 终止遍历
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------- 序列化方法(兼容原有逻辑) --------------------------
|
||||||
func (m *CsMap[K, V]) MarshalJSON() ([]byte, error) {
|
func (m *CsMap[K, V]) MarshalJSON() ([]byte, error) {
|
||||||
tmp := make(map[K]V, m.Count())
|
tmp := make(map[K]V, m.Count())
|
||||||
m.Range(func(key K, value V) (stop bool) {
|
m.Range(func(key K, value V) (stop bool) {
|
||||||
@@ -226,71 +200,18 @@ func (m *CsMap[K, V]) MarshalJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *CsMap[K, V]) UnmarshalJSON(b []byte) error {
|
func (m *CsMap[K, V]) UnmarshalJSON(b []byte) error {
|
||||||
tmp := make(map[K]V, m.Count())
|
tmp := make(map[K]V)
|
||||||
|
|
||||||
if err := json.Unmarshal(b, &tmp); err != nil {
|
if err := json.Unmarshal(b, &tmp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空原有数据
|
||||||
|
m.Clear()
|
||||||
|
// 批量存储
|
||||||
for key, val := range tmp {
|
for key, val := range tmp {
|
||||||
m.Store(key, val)
|
m.Store(key, val)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CsMap[K, V]) produce(ctx context.Context, ch chan Tuple[K, V]) {
|
// -------------------------- 移除所有无用的旧方法(produce/listen 等) --------------------------
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(len(m.shards))
|
|
||||||
for i := range m.shards {
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
|
||||||
// 1. 打印错误信息
|
|
||||||
|
|
||||||
cool.Logger.Error(context.TODO(), "csmap panic 错误:", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
shard := m.shards[i]
|
|
||||||
shard.RLock()
|
|
||||||
shard.items.Iter(func(k K, v V) (stop bool) {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
ch <- Tuple[K, V]{Key: k, Val: v}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
shard.RUnlock()
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(ch)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *CsMap[K, V]) listen(f func(key K, value V) (stop bool), ch chan Tuple[K, V]) *sync.WaitGroup {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
|
||||||
// 1. 打印错误信息
|
|
||||||
|
|
||||||
cool.Logger.Error(context.TODO(), " csmap panic 错误:", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for t := range ch {
|
|
||||||
if stop := f(t.Key, t.Val); stop {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return &wg
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,33 +2,4 @@ module github.com/zmexing/go-sensitive-word
|
|||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require github.com/orcaman/concurrent-map/v2 v2.0.1
|
||||||
github.com/imroc/req/v3 v3.42.3
|
|
||||||
github.com/orcaman/concurrent-map/v2 v2.0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
|
||||||
github.com/cloudflare/circl v1.3.7 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
|
||||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
|
||||||
github.com/onsi/ginkgo/v2 v2.16.0 // indirect
|
|
||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
|
||||||
github.com/quic-go/quic-go v0.40.1 // indirect
|
|
||||||
github.com/refraction-networking/utls v1.6.3 // indirect
|
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
|
||||||
go.uber.org/mock v0.4.0 // indirect
|
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
|
||||||
golang.org/x/net v0.33.0 // indirect
|
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
|
||||||
golang.org/x/text v0.22.0 // indirect
|
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,55 +1,2 @@
|
|||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
|
||||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
|
||||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
|
|
||||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
|
||||||
github.com/imroc/req/v3 v3.42.3 h1:ryPG2AiwouutAopwPxKpWKyxgvO8fB3hts4JXlh3PaE=
|
|
||||||
github.com/imroc/req/v3 v3.42.3/go.mod h1:Axz9Y/a2b++w5/Jht3IhQsdBzrG1ftJd1OJhu21bB2Q=
|
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
|
|
||||||
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
|
|
||||||
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
|
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
|
||||||
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
|
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
|
||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
|
||||||
github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q=
|
|
||||||
github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
|
|
||||||
github.com/refraction-networking/utls v1.6.3 h1:MFOfRN35sSx6K5AZNIoESsBuBxS2LCgRilRIdHb6fDc=
|
|
||||||
github.com/refraction-networking/utls v1.6.3/go.mod h1:yil9+7qSl+gBwJqztoQseO6Pr3h62pQoY1lXiNR/FPs=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
|
||||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
|
||||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
|
||||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
|
||||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"fmt"
|
||||||
"github.com/imroc/req/v3"
|
"time"
|
||||||
cmap "github.com/orcaman/concurrent-map/v2"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
cmap "github.com/orcaman/concurrent-map/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MemoryModel 使用并发 map 实现的内存词库
|
// MemoryModel 使用并发 map 实现的内存词库
|
||||||
@@ -62,26 +64,40 @@ func (m *MemoryModel) LoadDictEmbed(contents ...string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 从远程 HTTP 地址加载词库
|
// 从远程 HTTP 地址加载词库
|
||||||
|
// LoadDictHttp 批量从 HTTP 地址加载字典(标准库 net/http 实现)
|
||||||
func (m *MemoryModel) LoadDictHttp(urls ...string) error {
|
func (m *MemoryModel) LoadDictHttp(urls ...string) error {
|
||||||
|
// 【标准库】创建带超时的客户端,防止请求卡死
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second, // 超时控制,非常重要
|
||||||
|
}
|
||||||
|
|
||||||
for _, url := range urls {
|
for _, url := range urls {
|
||||||
err := func(url string) error {
|
// 立即执行函数,解决 defer 循环变量问题
|
||||||
httpRes, err := req.Get(url)
|
err := func(u string) error {
|
||||||
|
// 标准库 GET 请求
|
||||||
|
resp, err := client.Get(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("请求失败 %s: %w", u, err)
|
||||||
}
|
|
||||||
if httpRes == nil {
|
|
||||||
return errors.New("nil http response")
|
|
||||||
}
|
|
||||||
if httpRes.StatusCode != http.StatusOK {
|
|
||||||
return errors.New(httpRes.GetStatus())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func(Body io.ReadCloser) {
|
// 必须 defer 关闭 body,防止资源泄漏(标准库固定写法)
|
||||||
_ = Body.Close()
|
defer func() {
|
||||||
}(httpRes.Body)
|
closeErr := resp.Body.Close()
|
||||||
|
if closeErr != nil {
|
||||||
|
fmt.Printf("警告: 关闭响应体失败 url=%s, err=%v\n", u, closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return m.LoadDict(httpRes.Body)
|
// 状态码判断
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("http 状态码错误 url=%s, code=%d", u, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载字典(和你原来逻辑一样)
|
||||||
|
return m.LoadDict(resp.Body)
|
||||||
}(url)
|
}(url)
|
||||||
|
|
||||||
|
// 任意一个失败,立即返回
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,62 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
"github.com/gogf/gf/v2/util/gconv"
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
"github.com/gogf/gf/v2/util/grand"
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// IsToday 判断给定时间是否是今天
|
||||||
|
func IsToday(t1 *gtime.Time) bool {
|
||||||
|
if t1 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t := t1.Time
|
||||||
|
|
||||||
|
// 获取当前时间
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// 比较年、月、日是否相同
|
||||||
|
return t.Year() == now.Year() &&
|
||||||
|
t.Month() == now.Month() &&
|
||||||
|
t.Day() == now.Day()
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsWEEK(t1 *gtime.Time) bool {
|
||||||
|
if t1 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t := t1.Time
|
||||||
|
|
||||||
|
// 获取当前时间
|
||||||
|
now := time.Now()
|
||||||
|
_, nweek := now.ISOWeek()
|
||||||
|
_, tweek := now.ISOWeek()
|
||||||
|
// 比较年、月、日是否相同
|
||||||
|
return t.Year() == now.Year() &&
|
||||||
|
tweek == nweek
|
||||||
|
|
||||||
|
}
|
||||||
|
func IsMon(t1 *gtime.Time) bool {
|
||||||
|
if t1 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t := t1.Time
|
||||||
|
|
||||||
|
// 获取当前时间
|
||||||
|
now := time.Now()
|
||||||
|
nweek := now.Month()
|
||||||
|
tweek := now.Month()
|
||||||
|
// 比较年、月、日是否相同
|
||||||
|
return t.Year() == now.Year() &&
|
||||||
|
tweek == nweek
|
||||||
|
|
||||||
|
}
|
||||||
func FindWithIndex[T any](slice []T, predicate func(item T) bool) (int, *T, bool) {
|
func FindWithIndex[T any](slice []T, predicate func(item T) bool) (int, *T, bool) {
|
||||||
for i := range slice {
|
for i := range slice {
|
||||||
if predicate(slice[i]) {
|
if predicate(slice[i]) {
|
||||||
@@ -32,53 +83,71 @@ func RemoveLast(s string) string {
|
|||||||
return string(runes[:len(runes)-1])
|
return string(runes[:len(runes)-1])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ************************** 函数1:核心函数(双数组+int型概率,泛型)**************************
|
// RandomByWeight 根据整数权重随机选择元素(优化后的泛型版本)
|
||||||
// randomByIntProbs 核心概率计算函数:接收任意类型元素数组 + int型概率数组,实现权重随机
|
// 入参:
|
||||||
// T any:支持任意类型的元素切片,职责单一,只处理纯int概率的核心逻辑
|
// - elements: 待随机的元素集合(非空)
|
||||||
func RandomByIntProbs[T any](natureSet []T, probs []int) (T, error) {
|
// - weights: 对应元素的权重(非负整数,长度需与elements一致;长度不匹配/总权重为0时降级为等概率随机)
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// - 随机选中的元素
|
||||||
|
// - 错误(仅当elements为空/权重值为负时返回)
|
||||||
|
func RandomByWeight[Element any, Weight integer](elements []Element, weights []Weight) (Element, error) {
|
||||||
// 定义泛型零值,用于错误返回
|
// 定义泛型零值,用于错误返回
|
||||||
var zeroT T
|
var zero Element
|
||||||
|
|
||||||
// 1. 合法性校验:元素数组为空 或 概率数组与元素数组长度不匹配
|
// 1. 核心合法性校验:元素集合不能为空
|
||||||
if len(natureSet) == 0 {
|
if len(elements) == 0 {
|
||||||
return zeroT, errors.New("natureSet is empty")
|
return zero, errors.New("elements set is empty (cannot random from empty slice)")
|
||||||
}
|
|
||||||
if len(probs) == 0 || len(natureSet) != len(probs) {
|
|
||||||
// 长度不匹配,降级为等概率随机(兼容原有逻辑)
|
|
||||||
return natureSet[grand.Intn(len(natureSet))], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 校验概率值非负,并计算总概率
|
// 2. 权重数组合法性校验:长度不匹配/为空时,降级为等概率随机(兼容原逻辑)
|
||||||
totalProb := 0
|
elemLen := len(elements)
|
||||||
for i, p := range probs {
|
if len(weights) == 0 || len(weights) != elemLen {
|
||||||
if p < 0 {
|
return elements[rand.IntN(elemLen)], nil
|
||||||
return zeroT, errors.New("invalid prob value: index " + gconv.String(i) + " (must be non-negative integer)")
|
}
|
||||||
|
|
||||||
|
// 3. 校验权重非负,并计算总权重(统一转为int64避免溢出)
|
||||||
|
var totalWeight int64
|
||||||
|
// 预转换权重为int64,避免重复类型转换
|
||||||
|
intWeights := make([]int64, elemLen)
|
||||||
|
for i, w := range weights {
|
||||||
|
intW := int64(w)
|
||||||
|
if intW < 0 {
|
||||||
|
return zero, fmt.Errorf("invalid negative weight at index %d (value: %d)", i, w)
|
||||||
}
|
}
|
||||||
totalProb += p
|
intWeights[i] = intW
|
||||||
|
totalWeight += intW
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 总概率为0,降级为等概率随机
|
// 4. 总权重为0时,降级为等概率随机
|
||||||
if totalProb == 0 {
|
if totalWeight == 0 {
|
||||||
return natureSet[grand.Intn(len(natureSet))], nil
|
return elements[rand.IntN(elemLen)], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 计算前缀和(权重随机核心逻辑)
|
// 5. 计算前缀和(权重随机核心逻辑,复用预转换的int64权重)
|
||||||
prefixSum := make([]int, len(probs))
|
prefixWeights := make([]int64, elemLen)
|
||||||
prefixSum[0] = probs[0]
|
prefixWeights[0] = intWeights[0]
|
||||||
for i := 1; i < len(probs); i++ {
|
for i := 1; i < elemLen; i++ {
|
||||||
prefixSum[i] = prefixSum[i-1] + probs[i]
|
prefixWeights[i] = prefixWeights[i-1] + intWeights[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 生成随机数匹配前缀和,返回对应元素
|
// 6. 生成随机数并匹配前缀和(用int64避免溢出)
|
||||||
randVal := grand.Intn(totalProb)
|
randomValue := grand.Intn(int(totalWeight))
|
||||||
for i, sum := range prefixSum {
|
for i, sum := range prefixWeights {
|
||||||
if randVal < sum {
|
if randomValue < int(sum) {
|
||||||
return natureSet[i], nil
|
return elements[i], nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 极端情况兜底,返回第一个元素
|
// 极端兜底:理论上不会走到这里(randomValue < totalWeight),返回第一个元素保证不panic
|
||||||
return natureSet[0], nil
|
return elements[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// integer 自定义泛型约束:匹配所有整数类型(扩展原~int的限制)
|
||||||
|
// 包含:int/int8/int16/int32/int64/uint/uint8/uint16/uint32/uint64/uintptr
|
||||||
|
type integer interface {
|
||||||
|
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||||
|
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||||
}
|
}
|
||||||
|
|
||||||
// ************************** 函数2:封装函数(兼容string型概率,泛型)**************************
|
// ************************** 函数2:封装函数(兼容string型概率,泛型)**************************
|
||||||
@@ -89,7 +158,7 @@ func RandomByProbs[T any](natureSet []T, probs []string) (T, error) {
|
|||||||
|
|
||||||
// 1. 若string概率数组为空,直接调用核心函数(核心函数会处理降级逻辑)
|
// 1. 若string概率数组为空,直接调用核心函数(核心函数会处理降级逻辑)
|
||||||
if len(probs) == 0 {
|
if len(probs) == 0 {
|
||||||
return RandomByIntProbs(natureSet, []int{})
|
return RandomByWeight(natureSet, []int{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. string[] 转换为 int[](使用 gconv.Int 完成转换)
|
// 2. string[] 转换为 int[](使用 gconv.Int 完成转换)
|
||||||
@@ -100,5 +169,42 @@ func RandomByProbs[T any](natureSet []T, probs []string) (T, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 调用核心函数,复用概率计算逻辑
|
// 3. 调用核心函数,复用概率计算逻辑
|
||||||
return RandomByIntProbs(natureSet, probInts)
|
return RandomByWeight(natureSet, probInts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCurrentTimeInRange 判断当前时间是否在 startStr 和 endStr 表示的时间区间内(格式:HH:MM)
|
||||||
|
// 返回值:true=在区间内,false=不在区间内,error=时间解析失败
|
||||||
|
func IsCurrentTimeInRange(startStr, endStr string) (bool, error) {
|
||||||
|
// 1. 解析开始和结束时间字符串为 time.Time 对象(日期用当前日期)
|
||||||
|
now := time.Now()
|
||||||
|
location := now.Location() // 使用当前时区,避免时区偏差
|
||||||
|
|
||||||
|
// 解析开始时间(HH:MM)
|
||||||
|
startTime, err := time.ParseInLocation("15:04", startStr, location)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("解析开始时间 %s 失败:%w", startStr, err)
|
||||||
|
}
|
||||||
|
// 解析结束时间(HH:MM)
|
||||||
|
endTime, err := time.ParseInLocation("15:04", endStr, location)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("解析结束时间 %s 失败:%w", endStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 把开始/结束时间的日期替换为当前日期(只保留时分)
|
||||||
|
startToday := time.Date(now.Year(), now.Month(), now.Day(),
|
||||||
|
startTime.Hour(), startTime.Minute(), 0, 0, location)
|
||||||
|
endToday := time.Date(now.Year(), now.Month(), now.Day(),
|
||||||
|
endTime.Hour(), endTime.Minute(), 0, 0, location)
|
||||||
|
|
||||||
|
// 3. 比较当前时间是否在 [startToday, endToday] 区间内
|
||||||
|
return now.After(startToday) && now.Before(endToday), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format 把 args 按顺序填入 {0},{1},{2}...
|
||||||
|
func Format(s string, args ...interface{}) string {
|
||||||
|
for i, arg := range args {
|
||||||
|
placeholder := "{" + string(rune('0'+i)) + "}"
|
||||||
|
s = strings.ReplaceAll(s, placeholder, gconv.String(arg))
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,7 @@ type DoubleUInt8Struct struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (di *DoubleUInt8) Pack(p []byte, opt *Options) (int, error) {
|
func (di *DoubleUInt8) Pack(p []byte, opt *Options) (int, error) {
|
||||||
for i, value := range *di {
|
copy(p, (*di)[:])
|
||||||
p[i] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return 2, nil
|
return 2, nil
|
||||||
}
|
}
|
||||||
@@ -131,9 +129,7 @@ type SliceUInt8Struct struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ia *SliceUInt8) Pack(p []byte, opt *Options) (int, error) {
|
func (ia *SliceUInt8) Pack(p []byte, opt *Options) (int, error) {
|
||||||
for i, value := range *ia {
|
copy(p, *ia)
|
||||||
p[i] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(*ia) + 1, nil
|
return len(*ia) + 1, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Field struct {
|
type Field struct {
|
||||||
@@ -76,40 +77,60 @@ func (f *Field) Size(val reflect.Value, options *Options) int {
|
|||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) packVal(buf []byte, val reflect.Value, length int, options *Options) (size int, err error) {
|
// 预定义常用的order,避免重复判断
|
||||||
|
var defaultOrder = binary.BigEndian
|
||||||
|
|
||||||
|
// packVal 优化版:减少反射开销+优化内存拷贝+优雅错误处理
|
||||||
|
func (f *Field) packVal(buf []byte, val reflect.Value, _ int, options *Options) (size int, err error) {
|
||||||
|
// 1. 预缓存order,避免重复判断
|
||||||
order := f.Order
|
order := f.Order
|
||||||
if options.Order != nil {
|
if options != nil && options.Order != nil {
|
||||||
order = options.Order
|
order = options.Order
|
||||||
}
|
}
|
||||||
if f.Ptr {
|
if order == nil {
|
||||||
val = val.Elem()
|
order = defaultOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 处理指针类型:提前解引用,避免后续重复操作
|
||||||
|
if f.Ptr {
|
||||||
|
if !val.IsNil() {
|
||||||
|
val = val.Elem()
|
||||||
|
} else {
|
||||||
|
return 0, fmt.Errorf("field %s is nil pointer", f.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 预解析类型,避免重复Resolve
|
||||||
typ := f.Type.Resolve(options)
|
typ := f.Type.Resolve(options)
|
||||||
|
kind := val.Kind()
|
||||||
|
|
||||||
|
// 4. 扁平化分支逻辑,减少嵌套
|
||||||
switch typ {
|
switch typ {
|
||||||
case Struct:
|
case Struct:
|
||||||
return f.Fields.Pack(buf, val, options)
|
return f.Fields.Pack(buf, val, options)
|
||||||
|
|
||||||
case Bool, Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64:
|
case Bool, Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64:
|
||||||
size = typ.Size()
|
size = typ.Size()
|
||||||
|
if len(buf) < size {
|
||||||
|
return 0, fmt.Errorf("buf size %d < required %d", len(buf), size)
|
||||||
|
}
|
||||||
|
|
||||||
var n uint64
|
var n uint64
|
||||||
switch f.kind {
|
switch kind {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
if val.Bool() {
|
n = boolToUint64(val.Bool())
|
||||||
n = 1
|
|
||||||
} else {
|
|
||||||
n = 0
|
|
||||||
}
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
n = uint64(val.Int())
|
n = uint64(val.Int())
|
||||||
default:
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
n = val.Uint()
|
n = val.Uint()
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unsupported kind %s for numeric type %s", kind, typ)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 扁平化数值写入逻辑
|
||||||
switch typ {
|
switch typ {
|
||||||
case Bool:
|
case Bool:
|
||||||
if n != 0 {
|
buf[0] = byte(n)
|
||||||
buf[0] = 1
|
|
||||||
} else {
|
|
||||||
buf[0] = 0
|
|
||||||
}
|
|
||||||
case Int8, Uint8:
|
case Int8, Uint8:
|
||||||
buf[0] = byte(n)
|
buf[0] = byte(n)
|
||||||
case Int16, Uint16:
|
case Int16, Uint16:
|
||||||
@@ -117,33 +138,90 @@ func (f *Field) packVal(buf []byte, val reflect.Value, length int, options *Opti
|
|||||||
case Int32, Uint32:
|
case Int32, Uint32:
|
||||||
order.PutUint32(buf, uint32(n))
|
order.PutUint32(buf, uint32(n))
|
||||||
case Int64, Uint64:
|
case Int64, Uint64:
|
||||||
order.PutUint64(buf, uint64(n))
|
order.PutUint64(buf, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
case Float32, Float64:
|
case Float32, Float64:
|
||||||
size = typ.Size()
|
size = typ.Size()
|
||||||
|
if len(buf) < size {
|
||||||
|
return 0, fmt.Errorf("buf size %d < required %d", len(buf), size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind != reflect.Float32 && kind != reflect.Float64 {
|
||||||
|
return 0, fmt.Errorf("unsupported kind %s for float type %s", kind, typ)
|
||||||
|
}
|
||||||
n := val.Float()
|
n := val.Float()
|
||||||
|
|
||||||
switch typ {
|
switch typ {
|
||||||
case Float32:
|
case Float32:
|
||||||
order.PutUint32(buf, math.Float32bits(float32(n)))
|
order.PutUint32(buf, math.Float32bits(float32(n)))
|
||||||
case Float64:
|
case Float64:
|
||||||
order.PutUint64(buf, math.Float64bits(n))
|
order.PutUint64(buf, math.Float64bits(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
case String:
|
case String:
|
||||||
switch f.kind {
|
// 优化String类型:减少内存拷贝
|
||||||
|
switch kind {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
|
s := val.String()
|
||||||
|
size = len(s)
|
||||||
|
if len(buf) < size {
|
||||||
|
return 0, fmt.Errorf("buf size %d < string length %d", len(buf), size)
|
||||||
|
}
|
||||||
|
// 用unsafe直接拷贝字符串到buf,避免[]byte(s)的内存分配
|
||||||
|
copyStringToBuf(buf, s)
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
if val.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
return 0, fmt.Errorf("unsupported slice type %s for String field", val.Type())
|
||||||
|
}
|
||||||
size = val.Len()
|
size = val.Len()
|
||||||
copy(buf, []byte(val.String()))
|
if len(buf) < size {
|
||||||
default:
|
return 0, fmt.Errorf("buf size %d < bytes length %d", len(buf), size)
|
||||||
// TODO: handle kind != bytes here
|
}
|
||||||
size = val.Len()
|
// 直接拷贝字节切片,避免冗余操作
|
||||||
copy(buf, val.Bytes())
|
copy(buf, val.Bytes())
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unsupported kind %s for String type", kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
case CustomType:
|
case CustomType:
|
||||||
return val.Addr().Interface().(Custom).Pack(buf, options)
|
// 优化反射断言:提前检查类型,避免panic
|
||||||
|
if !val.CanAddr() {
|
||||||
|
return 0, fmt.Errorf("custom type %s cannot take address", val.Type())
|
||||||
|
}
|
||||||
|
custom, ok := val.Addr().Interface().(Custom)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("type %s does not implement Custom interface", val.Type())
|
||||||
|
}
|
||||||
|
return custom.Pack(buf, options)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("no pack handler for type: %s", typ))
|
// 替换panic为error,避免程序崩溃
|
||||||
|
return 0, fmt.Errorf("no pack handler for type: %s", typ)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:bool转uint64,减少inline重复代码
|
||||||
|
func boolToUint64(b bool) uint64 {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:unsafe拷贝字符串到buf,避免[]byte(s)的内存分配
|
||||||
|
// 注意:仅在确定buf长度足够时使用
|
||||||
|
func copyStringToBuf(buf []byte, s string) {
|
||||||
|
// unsafe转换:字符串转字节切片,无内存分配
|
||||||
|
src := *(*[]byte)(unsafe.Pointer(&struct {
|
||||||
|
string
|
||||||
|
cap int
|
||||||
|
}{s, len(s)}))
|
||||||
|
copy(buf, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) Pack(buf []byte, val reflect.Value, length int, options *Options) (int, error) {
|
func (f *Field) Pack(buf []byte, val reflect.Value, length int, options *Options) (int, error) {
|
||||||
@@ -174,7 +252,6 @@ func (f *Field) Pack(buf []byte, val reflect.Value, length int, options *Options
|
|||||||
copy(buf, buf[:length])
|
copy(buf, buf[:length])
|
||||||
return length, nil
|
return length, nil
|
||||||
}
|
}
|
||||||
return val.Len(), nil
|
|
||||||
}
|
}
|
||||||
pos := 0
|
pos := 0
|
||||||
var zero reflect.Value
|
var zero reflect.Value
|
||||||
@@ -198,7 +275,7 @@ func (f *Field) Pack(buf []byte, val reflect.Value, length int, options *Options
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) unpackVal(buf []byte, val reflect.Value, length int, options *Options) error {
|
func (f *Field) unpackVal(buf []byte, val reflect.Value, _ int, options *Options) error {
|
||||||
order := f.Order
|
order := f.Order
|
||||||
if options.Order != nil {
|
if options.Order != nil {
|
||||||
order = options.Order
|
order = options.Order
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package struc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -103,76 +104,188 @@ func (f Fields) Pack(buf []byte, val reflect.Value, options *Options) (int, erro
|
|||||||
return pos, nil
|
return pos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取魔法常量,便于配置和维护
|
||||||
|
const (
|
||||||
|
// MaxBufferSize 最大缓冲区大小,防止分配过大内存
|
||||||
|
MaxBufferSize = 1024 * 1024 // 1MB
|
||||||
|
// smallBufferSize 小缓冲区大小,复用栈上数组减少堆分配
|
||||||
|
smallBufferSize = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
// -------------------------- 优化后的核心方法 --------------------------
|
||||||
func (f Fields) Unpack(r io.Reader, val reflect.Value, options *Options) error {
|
func (f Fields) Unpack(r io.Reader, val reflect.Value, options *Options) error {
|
||||||
|
// 解引用指针,直到拿到非指针类型的值
|
||||||
for val.Kind() == reflect.Ptr {
|
for val.Kind() == reflect.Ptr {
|
||||||
val = val.Elem()
|
val = val.Elem()
|
||||||
}
|
}
|
||||||
var tmp [8]byte
|
|
||||||
var buf []byte
|
// 定义小缓冲区(栈上分配,减少堆内存开销)
|
||||||
for i, field := range f {
|
var smallBuf [smallBufferSize]byte
|
||||||
|
var readBuf []byte
|
||||||
|
|
||||||
|
// 遍历所有字段
|
||||||
|
for fieldIdx, field := range f {
|
||||||
|
// 跳过空字段
|
||||||
if field == nil {
|
if field == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
v := val.Field(i)
|
|
||||||
length := field.Len
|
// 获取当前字段的反射值
|
||||||
|
fieldVal := val.Field(fieldIdx)
|
||||||
|
// 获取字段长度(优先从Sizefrom读取,否则用默认Len)
|
||||||
|
fieldLen := field.Len
|
||||||
if field.Sizefrom != nil {
|
if field.Sizefrom != nil {
|
||||||
length = f.sizefrom(val, field.Sizefrom)
|
fieldLen = f.sizefrom(val, field.Sizefrom)
|
||||||
}
|
}
|
||||||
if v.Kind() == reflect.Ptr && !v.Elem().IsValid() {
|
|
||||||
v.Set(reflect.New(v.Type().Elem()))
|
// 处理指针字段:如果指针未初始化,创建新实例
|
||||||
|
if fieldVal.Kind() == reflect.Ptr && !fieldVal.Elem().IsValid() {
|
||||||
|
fieldVal.Set(reflect.New(fieldVal.Type().Elem()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理结构体类型字段
|
||||||
if field.Type == Struct {
|
if field.Type == Struct {
|
||||||
if field.Slice {
|
if err := f.unpackStructField(r, fieldVal, fieldLen, field, options); err != nil {
|
||||||
vals := v
|
return fmt.Errorf("unpack struct field index %d: %w", fieldIdx, err)
|
||||||
if !field.Array {
|
|
||||||
vals = reflect.MakeSlice(v.Type(), length, length)
|
|
||||||
}
|
|
||||||
for i := 0; i < length; i++ {
|
|
||||||
v := vals.Index(i)
|
|
||||||
fields, err := parseFields(v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fields.Unpack(r, v, options); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !field.Array {
|
|
||||||
v.Set(vals)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO: DRY (we repeat the inner loop above)
|
|
||||||
fields, err := parseFields(v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fields.Unpack(r, v, options); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理非结构体类型字段(基础类型/自定义类型)
|
||||||
|
if err := f.unpackBasicField(r, fieldVal, field, fieldLen, smallBuf[:], &readBuf, options); err != nil {
|
||||||
|
return fmt.Errorf("unpack basic field index %d: %w", fieldIdx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义全局的最大安全切片长度(可根据业务调整,建议通过 options 配置)
|
||||||
|
const defaultMaxSafeSliceLen = 10000 // 1万,根据实际场景调整
|
||||||
|
|
||||||
|
// 新增错误类型,便于上层捕获
|
||||||
|
var (
|
||||||
|
ErrExceedMaxSliceLen = errors.New("slice length exceeds maximum safe limit")
|
||||||
|
ErrInvalidSliceLen = errors.New("slice length is negative or zero")
|
||||||
|
)
|
||||||
|
|
||||||
|
// unpackStructField 抽离重复的结构体解析逻辑,解决DRY问题
|
||||||
|
// 修复点:增加长度校验和内存分配防护
|
||||||
|
func (f Fields) unpackStructField(r io.Reader, fieldVal reflect.Value, length int, field *Field, options *Options) error {
|
||||||
|
// 修复1:基础长度校验,拒绝无效/超大长度
|
||||||
|
if length <= 0 {
|
||||||
|
return ErrInvalidSliceLen
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复2:获取最大允许的切片长度(优先使用 options 配置,无则用默认值)
|
||||||
|
maxSliceLen := defaultMaxSafeSliceLen
|
||||||
|
|
||||||
|
// 修复3:校验长度是否超过安全阈值,防止OOM
|
||||||
|
if length > maxSliceLen {
|
||||||
|
return fmt.Errorf("%w: requested %d, max allowed %d", ErrExceedMaxSliceLen, length, maxSliceLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理切片/数组类型的结构体字段
|
||||||
|
if field.Slice {
|
||||||
|
var sliceVal reflect.Value
|
||||||
|
// 如果是数组(固定长度),直接使用原字段;如果是切片,创建指定长度的切片
|
||||||
|
if field.Array {
|
||||||
|
sliceVal = fieldVal
|
||||||
} else {
|
} else {
|
||||||
typ := field.Type.Resolve(options)
|
// 原逻辑:这里是OOM的核心触发点,现在已经提前做了长度校验
|
||||||
if typ == CustomType {
|
sliceVal = reflect.MakeSlice(fieldVal.Type(), length, length)
|
||||||
if err := v.Addr().Interface().(Custom).Unpack(r, length, options); err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
// 遍历切片/数组的每个元素,解析结构体
|
||||||
} else {
|
for elemIdx := 0; elemIdx < length; elemIdx++ {
|
||||||
size := length * field.Type.Resolve(options).Size()
|
elemVal := sliceVal.Index(elemIdx)
|
||||||
if size < 8 {
|
if err := f.unpackSingleStructElem(r, elemVal, options); err != nil {
|
||||||
buf = tmp[:size]
|
return fmt.Errorf("slice elem %d: %w", elemIdx, err)
|
||||||
} else {
|
|
||||||
buf = make([]byte, size)
|
|
||||||
}
|
|
||||||
if _, err := io.ReadFull(r, buf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err := field.Unpack(buf[:size], v, length, options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 非数组类型需要将创建的切片赋值回原字段
|
||||||
|
if !field.Array {
|
||||||
|
fieldVal.Set(sliceVal)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 处理单个结构体字段
|
||||||
|
if err := f.unpackSingleStructElem(r, fieldVal, options); err != nil {
|
||||||
|
return fmt.Errorf("single struct: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------- 抽离的辅助方法:处理单个结构体元素 --------------------------
|
||||||
|
// unpackSingleStructElem 解析单个结构体元素的核心逻辑(原重复代码)
|
||||||
|
func (f Fields) unpackSingleStructElem(r io.Reader, elemVal reflect.Value, options *Options) error {
|
||||||
|
// 解析结构体的字段定义
|
||||||
|
structFields, err := parseFields(elemVal)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse struct fields: %w", err)
|
||||||
|
}
|
||||||
|
// 递归调用Unpack解析结构体数据
|
||||||
|
if err := structFields.Unpack(r, elemVal, options); err != nil {
|
||||||
|
return fmt.Errorf("unpack struct elem: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------- 抽离的辅助方法:处理基础类型字段 --------------------------
|
||||||
|
// unpackBasicField 处理非结构体类型的字段(基础类型/自定义类型)
|
||||||
|
func (f Fields) unpackBasicField(
|
||||||
|
r io.Reader,
|
||||||
|
fieldVal reflect.Value,
|
||||||
|
field *Field,
|
||||||
|
length int,
|
||||||
|
smallBuf []byte,
|
||||||
|
readBuf *[]byte,
|
||||||
|
options *Options,
|
||||||
|
) error {
|
||||||
|
// 解析字段实际类型
|
||||||
|
fieldType := field.Type.Resolve(options)
|
||||||
|
|
||||||
|
// 处理自定义类型(实现Custom接口)
|
||||||
|
if fieldType == CustomType {
|
||||||
|
custom, ok := fieldVal.Addr().Interface().(Custom)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("field does not implement Custom interface")
|
||||||
|
}
|
||||||
|
if err := custom.Unpack(r, length, options); err != nil {
|
||||||
|
return fmt.Errorf("custom unpack: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算需要读取的字节数
|
||||||
|
bufferSize := length * fieldType.Size()
|
||||||
|
// 检查缓冲区大小,防止内存溢出
|
||||||
|
if bufferSize > MaxBufferSize {
|
||||||
|
return fmt.Errorf("buffer size %d exceeds max %d", bufferSize, MaxBufferSize)
|
||||||
|
}
|
||||||
|
// 长度为0时直接返回
|
||||||
|
if bufferSize <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复用小缓冲区(栈上)或分配堆缓冲区,减少内存分配开销
|
||||||
|
if bufferSize < smallBufferSize {
|
||||||
|
*readBuf = smallBuf[:bufferSize]
|
||||||
|
} else {
|
||||||
|
*readBuf = make([]byte, bufferSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取指定长度的字节数据
|
||||||
|
if _, err := io.ReadFull(r, *readBuf); err != nil {
|
||||||
|
return fmt.Errorf("read data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析字节数据到目标字段
|
||||||
|
if err := field.Unpack((*readBuf)[:bufferSize], fieldVal, length, options); err != nil {
|
||||||
|
return fmt.Errorf("field unpack: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -33,31 +34,88 @@ func init() {
|
|||||||
emptyOptions.Validate()
|
emptyOptions.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var prepCache = sync.Map{}
|
||||||
|
|
||||||
|
// cacheKey 缓存键:区分 结构体/自定义类型/二进制类型,保证缓存唯一性
|
||||||
|
type cacheKey struct {
|
||||||
|
typ reflect.Type // 数据的基础类型
|
||||||
|
kind uint8 // 0=结构体, 1=自定义类型, 2=二进制类型
|
||||||
|
}
|
||||||
|
|
||||||
|
// prep 优化版:带完整缓存,缓存处理后的最终 Packer
|
||||||
func prep(data interface{}) (reflect.Value, Packer, error) {
|
func prep(data interface{}) (reflect.Value, Packer, error) {
|
||||||
|
// 1. 提前判空
|
||||||
|
if data == nil {
|
||||||
|
return reflect.Value{}, nil, fmt.Errorf("Invalid reflect.Value for nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 初始反射值处理(和原逻辑一致)
|
||||||
value := reflect.ValueOf(data)
|
value := reflect.ValueOf(data)
|
||||||
for value.Kind() == reflect.Ptr {
|
elemValue := value
|
||||||
next := value.Elem().Kind()
|
for elemValue.Kind() == reflect.Ptr {
|
||||||
|
next := elemValue.Elem().Kind()
|
||||||
if next == reflect.Struct || next == reflect.Ptr {
|
if next == reflect.Struct || next == reflect.Ptr {
|
||||||
value = value.Elem()
|
elemValue = elemValue.Elem()
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch value.Kind() {
|
|
||||||
|
// 3. 构建缓存键的基础类型(取解引用后的类型)
|
||||||
|
baseType := elemValue.Type()
|
||||||
|
var packer Packer
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// 4. 按类型分支处理,优先查缓存
|
||||||
|
switch elemValue.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
fields, err := parseFields(value)
|
// 缓存键:结构体类型
|
||||||
return value, fields, err
|
key := cacheKey{typ: baseType, kind: 0}
|
||||||
|
if cacheVal, ok := prepCache.Load(key); ok {
|
||||||
|
// 缓存命中:直接返回缓存的 Packer
|
||||||
|
return elemValue, cacheVal.(Packer), nil
|
||||||
|
}
|
||||||
|
// 缓存未命中:执行原逻辑解析 fields
|
||||||
|
packer, err = parseFields(elemValue)
|
||||||
|
if err != nil {
|
||||||
|
return elemValue, nil, err
|
||||||
|
}
|
||||||
|
// 缓存处理后的 Packer
|
||||||
|
prepCache.Store(key, packer)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if !value.IsValid() {
|
// 非结构体类型:检查有效性
|
||||||
|
if !elemValue.IsValid() {
|
||||||
return reflect.Value{}, nil, fmt.Errorf("Invalid reflect.Value for %+v", data)
|
return reflect.Value{}, nil, fmt.Errorf("Invalid reflect.Value for %+v", data)
|
||||||
}
|
}
|
||||||
if c, ok := data.(Custom); ok {
|
|
||||||
return value, customFallback{c}, nil
|
|
||||||
}
|
|
||||||
return value, binaryFallback(value), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 自定义类型分支
|
||||||
|
if c, ok := data.(Custom); ok {
|
||||||
|
// 缓存键:自定义类型
|
||||||
|
key := cacheKey{typ: baseType, kind: 1}
|
||||||
|
if cacheVal, ok := prepCache.Load(key); ok {
|
||||||
|
return elemValue, cacheVal.(Packer), nil
|
||||||
|
}
|
||||||
|
// 构建 customFallback 并缓存
|
||||||
|
// 仅用 custom Custom 构建,完全匹配你的定义
|
||||||
|
packer = customFallback{custom: c}
|
||||||
|
prepCache.Store(key, packer)
|
||||||
|
} else {
|
||||||
|
// 二进制类型分支
|
||||||
|
// 缓存键:二进制类型
|
||||||
|
key := cacheKey{typ: baseType, kind: 2}
|
||||||
|
if cacheVal, ok := prepCache.Load(key); ok {
|
||||||
|
return elemValue, cacheVal.(Packer), nil
|
||||||
|
}
|
||||||
|
// 构建 binaryFallback 并缓存
|
||||||
|
packer = binaryFallback(elemValue)
|
||||||
|
prepCache.Store(key, packer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 返回和原逻辑完全一致的结果
|
||||||
|
return elemValue, packer, err
|
||||||
|
}
|
||||||
func Pack(w io.Writer, data interface{}) error {
|
func Pack(w io.Writer, data interface{}) error {
|
||||||
return PackWithOptions(w, data, nil)
|
return PackWithOptions(w, data, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/apcera/termtables"
|
"github.com/apcera/termtables"
|
||||||
|
"github.com/gogf/gf/v2/os/gfile"
|
||||||
"github.com/gogf/gf/v2/os/glog"
|
"github.com/gogf/gf/v2/os/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,6 +38,12 @@ type Map struct {
|
|||||||
Galaxy string `xml:"galaxy,attr" json:"galaxy,omitempty"`
|
Galaxy string `xml:"galaxy,attr" json:"galaxy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Mapxml() {
|
||||||
|
|
||||||
|
superMaps := &SuperMaps{}
|
||||||
|
err := xml.Unmarshal([]byte(gfile.GetBytes("public/config/地图配置野怪.xml")), superMaps)
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
func TestXml(t *testing.T) {
|
func TestXml(t *testing.T) {
|
||||||
// 示例XML数据
|
// 示例XML数据
|
||||||
xmlData := `<?xml version="1.0" encoding="UTF-8"?>
|
xmlData := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|||||||
@@ -84,3 +84,18 @@ func RandomSlice[T any](slice []T, n int) []T {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// T: 切片元素类型(必须是可比较类型,满足map键的要求)
|
||||||
|
// 返回值:map[T]int - 键为切片元素,值为对应出现次数
|
||||||
|
func CountSliceElements[T comparable](slice []T) map[T]int {
|
||||||
|
// 初始化map,预设容量为切片长度(优化性能)
|
||||||
|
countMap := make(map[T]int, len(slice))
|
||||||
|
|
||||||
|
// 遍历切片,统计每个元素的出现次数
|
||||||
|
for _, v := range slice {
|
||||||
|
// 若元素已存在,值+1;不存在则自动初始化为0后+1
|
||||||
|
countMap[v]++
|
||||||
|
}
|
||||||
|
|
||||||
|
return countMap
|
||||||
|
}
|
||||||
|
|||||||
177
common/utils/zset/README.md
Normal file
177
common/utils/zset/README.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# ZSet
|
||||||
|
This Go package provides an implementation of sorted set in redis.
|
||||||
|
|
||||||
|
## Usage (go < 1.18)
|
||||||
|
All you have to do is to implement a comparison `function Less(Item) bool` and a `function Key() string` for your Item which will be store in the zset, here are some examples.
|
||||||
|
``` go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/liwnn/zset"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Name string
|
||||||
|
Score int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u User) Key() string {
|
||||||
|
return u.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u User) Less(than zset.Item) bool {
|
||||||
|
if u.Score == than.(User).Score {
|
||||||
|
return u.Name < than.(User).Name
|
||||||
|
}
|
||||||
|
return u.Score < than.(User).Score
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
zs := zset.New()
|
||||||
|
|
||||||
|
// Add
|
||||||
|
zs.Add("Hurst", User{Name: "Hurst", Score: 88})
|
||||||
|
zs.Add("Peek", User{Name: "Peek", Score: 100})
|
||||||
|
zs.Add("Beaty", User{Name: "Beaty", Score: 66})
|
||||||
|
|
||||||
|
// Rank
|
||||||
|
rank := zs.Rank("Hurst", true)
|
||||||
|
fmt.Printf("Hurst's rank is %v\n", rank) // expected 2
|
||||||
|
|
||||||
|
// Range
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Range[0,3]:")
|
||||||
|
zs.Range(0, 3, true, func(v zset.Item, rank int) bool {
|
||||||
|
fmt.Printf("%v's rank is %v\n", v.(User).Key(), rank)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Range with Iterator
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Range[0,3] with Iterator:")
|
||||||
|
for it := zs.RangeIterator(0, 3, true); it.Valid(); it.Next() {
|
||||||
|
fmt.Printf("Ite: %v's rank is %v\n", it.Item().(User).Key(), it.Rank())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range by score [88, 100]
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("RangeByScore[88,100]:")
|
||||||
|
zs.RangeByScore(func(i zset.Item) bool {
|
||||||
|
return i.(User).Score >= 88
|
||||||
|
}, func(i zset.Item) bool {
|
||||||
|
return i.(User).Score <= 100
|
||||||
|
}, true, func(i zset.Item, rank int) bool {
|
||||||
|
fmt.Printf("%v's score[%v] rank is %v\n", i.(User).Key(), i.(User).Score, rank)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove
|
||||||
|
zs.Remove("Peek")
|
||||||
|
|
||||||
|
// Rank
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("After remove Peek:")
|
||||||
|
rank = zs.Rank("Hurst", true)
|
||||||
|
fmt.Printf("Hurst's rank is %v\n", rank) // expected 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
Hurst's rank is 2
|
||||||
|
|
||||||
|
Range[0,3]:
|
||||||
|
Peek's rank is 1
|
||||||
|
Hurst's rank is 2
|
||||||
|
Beaty's rank is 3
|
||||||
|
|
||||||
|
Range[0,3] with Iterator:
|
||||||
|
Ite: Peek's rank is 1
|
||||||
|
Ite: Hurst's rank is 2
|
||||||
|
Ite: Beaty's rank is 3
|
||||||
|
|
||||||
|
RangeByScore[88,100]:
|
||||||
|
Peek's score[100] rank is 1
|
||||||
|
Hurst's score[88] rank is 2
|
||||||
|
|
||||||
|
After remove Peek:
|
||||||
|
Hurst's rank is 1
|
||||||
|
```
|
||||||
|
## Usage (go >= 1.18)
|
||||||
|
``` go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/liwnn/zset"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Name string
|
||||||
|
Score int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u User) Key() string {
|
||||||
|
return u.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u User) Less(than User) bool {
|
||||||
|
if u.Score == than.Score {
|
||||||
|
return u.Name < than.Name
|
||||||
|
}
|
||||||
|
return u.Score < than.Score
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
zs := zset.New[string, User](func(a, b User) bool {
|
||||||
|
return a.Less(b)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add
|
||||||
|
zs.Add("Hurst", User{Name: "Hurst", Score: 88})
|
||||||
|
zs.Add("Peek", User{Name: "Peek", Score: 100})
|
||||||
|
zs.Add("Beaty", User{Name: "Beaty", Score: 66})
|
||||||
|
|
||||||
|
// Rank
|
||||||
|
rank := zs.Rank("Hurst", true)
|
||||||
|
fmt.Printf("Hurst's rank is %v\n", rank) // expected 2
|
||||||
|
|
||||||
|
// Range
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Range[0,3]:")
|
||||||
|
zs.Range(0, 3, true, func(v User, rank int) bool {
|
||||||
|
fmt.Printf("%v's rank is %v\n", v.Key(), rank)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Range with Iterator
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Range[0,3] with Iterator:")
|
||||||
|
for it := zs.RangeIterator(0, 3, true); it.Valid(); it.Next() {
|
||||||
|
fmt.Printf("Ite: %v's rank is %v\n", it.Item().Key(), it.Rank())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range by score [88, 100]
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("RangeByScore[88,100]:")
|
||||||
|
zs.RangeByScore(func(i User) bool {
|
||||||
|
return i.Score >= 88
|
||||||
|
}, func(i User) bool {
|
||||||
|
return i.Score <= 100
|
||||||
|
}, true, func(i User, rank int) bool {
|
||||||
|
fmt.Printf("%v's score[%v] rank is %v\n", i.Key(), i.Score, rank)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove
|
||||||
|
zs.Remove("Peek")
|
||||||
|
|
||||||
|
// Rank
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("After remove Peek:")
|
||||||
|
rank = zs.Rank("Hurst", true)
|
||||||
|
fmt.Printf("Hurst's rank is %v\n", rank) // expected 1
|
||||||
|
}
|
||||||
|
```
|
||||||
3
common/utils/zset/go.mod
Normal file
3
common/utils/zset/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/liwnn/zset
|
||||||
|
|
||||||
|
go 1.18
|
||||||
540
common/utils/zset/zset.go
Normal file
540
common/utils/zset/zset.go
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
//go:build !go1.18
|
||||||
|
// +build !go1.18
|
||||||
|
|
||||||
|
// Package zset implements sorted set similar to redis zset.
|
||||||
|
package zset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultMaxLevel = 32 // (1/p)^MaxLevel >= maxNode
|
||||||
|
DefaultP = 0.25 // SkipList P = 1/4
|
||||||
|
|
||||||
|
DefaultFreeListSize = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// Item represents a single object in the set.
|
||||||
|
type Item interface {
|
||||||
|
// Less must provide a strict weak ordering
|
||||||
|
Less(Item) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemIterator allows callers of Range* to iterate of the zset.
|
||||||
|
// When this function returns false, iteration will stop.
|
||||||
|
type ItemIterator func(i Item, rank int) bool
|
||||||
|
|
||||||
|
type skipListLevel struct {
|
||||||
|
forward *node
|
||||||
|
span int
|
||||||
|
}
|
||||||
|
|
||||||
|
// node is an element of a skip list
|
||||||
|
type node struct {
|
||||||
|
item Item
|
||||||
|
backward *node
|
||||||
|
level []skipListLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreeList represents a free list of set node.
|
||||||
|
type FreeList struct {
|
||||||
|
freelist []*node
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFreeList creates a new free list.
|
||||||
|
func NewFreeList(size int) *FreeList {
|
||||||
|
return &FreeList{freelist: make([]*node, 0, size)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FreeList) newNode(lvl int) (n *node) {
|
||||||
|
if len(f.freelist) == 0 {
|
||||||
|
n = new(node)
|
||||||
|
n.level = make([]skipListLevel, lvl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
index := len(f.freelist) - 1
|
||||||
|
n = f.freelist[index]
|
||||||
|
f.freelist[index] = nil
|
||||||
|
f.freelist = f.freelist[:index]
|
||||||
|
|
||||||
|
if cap(n.level) < lvl {
|
||||||
|
n.level = make([]skipListLevel, lvl)
|
||||||
|
} else {
|
||||||
|
n.level = n.level[:lvl]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FreeList) freeNode(n *node) (out bool) {
|
||||||
|
// for gc
|
||||||
|
n.item = nil
|
||||||
|
for j := 0; j < len(n.level); j++ {
|
||||||
|
n.level[j] = skipListLevel{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(f.freelist) < cap(f.freelist) {
|
||||||
|
f.freelist = append(f.freelist, n)
|
||||||
|
out = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// skipList represents a skip list
|
||||||
|
type skipList struct {
|
||||||
|
header, tail *node
|
||||||
|
length int
|
||||||
|
level int // current level count
|
||||||
|
maxLevel int
|
||||||
|
freelist *FreeList
|
||||||
|
random *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSkipList creates a skip list
|
||||||
|
func newSkipList(maxLevel int) *skipList {
|
||||||
|
if maxLevel < DefaultMaxLevel {
|
||||||
|
panic("maxLevel must < 32")
|
||||||
|
}
|
||||||
|
return &skipList{
|
||||||
|
level: 1,
|
||||||
|
header: &node{
|
||||||
|
level: make([]skipListLevel, maxLevel),
|
||||||
|
},
|
||||||
|
maxLevel: maxLevel,
|
||||||
|
freelist: NewFreeList(DefaultFreeListSize),
|
||||||
|
random: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert an item into the SkipList.
|
||||||
|
func (sl *skipList) insert(item Item) *node {
|
||||||
|
var update [DefaultMaxLevel]*node // [0...list.maxLevel)
|
||||||
|
var rank [DefaultMaxLevel]int
|
||||||
|
x := sl.header
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
if i == sl.level-1 {
|
||||||
|
rank[i] = 0
|
||||||
|
} else {
|
||||||
|
rank[i] = rank[i+1]
|
||||||
|
}
|
||||||
|
for y := x.level[i].forward; y != nil && y.item.Less(item); y = x.level[i].forward {
|
||||||
|
rank[i] += x.level[i].span
|
||||||
|
x = y
|
||||||
|
}
|
||||||
|
update[i] = x
|
||||||
|
}
|
||||||
|
|
||||||
|
lvl := sl.randomLevel()
|
||||||
|
if lvl > sl.level {
|
||||||
|
for i := sl.level; i < lvl; i++ {
|
||||||
|
rank[i] = 0
|
||||||
|
update[i] = sl.header
|
||||||
|
update[i].level[i].span = sl.length
|
||||||
|
}
|
||||||
|
sl.level = lvl
|
||||||
|
}
|
||||||
|
|
||||||
|
x = sl.freelist.newNode(lvl)
|
||||||
|
x.item = item
|
||||||
|
for i := 0; i < lvl; i++ {
|
||||||
|
x.level[i].forward = update[i].level[i].forward
|
||||||
|
update[i].level[i].forward = x
|
||||||
|
|
||||||
|
x.level[i].span = update[i].level[i].span - (rank[0] - rank[i])
|
||||||
|
update[i].level[i].span = (rank[0] - rank[i]) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment span for untouched levels
|
||||||
|
for i := lvl; i < sl.level; i++ {
|
||||||
|
update[i].level[i].span++
|
||||||
|
}
|
||||||
|
|
||||||
|
if update[0] == sl.header {
|
||||||
|
x.backward = nil
|
||||||
|
} else {
|
||||||
|
x.backward = update[0]
|
||||||
|
}
|
||||||
|
if x.level[0].forward == nil {
|
||||||
|
sl.tail = x
|
||||||
|
} else {
|
||||||
|
x.level[0].forward.backward = x
|
||||||
|
}
|
||||||
|
sl.length++
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete element
|
||||||
|
func (sl *skipList) delete(n *node) Item {
|
||||||
|
var preAlloc [DefaultMaxLevel]*node // [0...list.maxLevel)
|
||||||
|
update := preAlloc[:sl.maxLevel]
|
||||||
|
x := sl.header
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
for y := x.level[i].forward; y != nil && y.item.Less(n.item); y = x.level[i].forward {
|
||||||
|
x = y
|
||||||
|
}
|
||||||
|
update[i] = x
|
||||||
|
}
|
||||||
|
x = x.level[0].forward
|
||||||
|
if x != nil && !n.item.Less(x.item) {
|
||||||
|
for i := 0; i < sl.level; i++ {
|
||||||
|
if update[i].level[i].forward == x {
|
||||||
|
update[i].level[i].span += x.level[i].span - 1
|
||||||
|
update[i].level[i].forward = x.level[i].forward
|
||||||
|
} else {
|
||||||
|
update[i].level[i].span--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for sl.level > 1 && sl.header.level[sl.level-1].forward == nil {
|
||||||
|
sl.level--
|
||||||
|
}
|
||||||
|
if x.level[0].forward == nil {
|
||||||
|
sl.tail = x.backward
|
||||||
|
} else {
|
||||||
|
x.level[0].forward.backward = x.backward
|
||||||
|
}
|
||||||
|
removeItem := x.item
|
||||||
|
sl.freelist.freeNode(x)
|
||||||
|
sl.length--
|
||||||
|
return removeItem
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *skipList) updateItem(node *node, item Item) bool {
|
||||||
|
if (node.level[0].forward == nil || !node.level[0].forward.item.Less(item)) &&
|
||||||
|
(node.backward == nil || !item.Less(node.backward.item)) {
|
||||||
|
node.item = item
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRank find the rank for an element.
|
||||||
|
// Returns 0 when the element cannot be found, rank otherwise.
|
||||||
|
// Note that the rank is 1-based
|
||||||
|
func (sl *skipList) getRank(item Item) int {
|
||||||
|
var rank int
|
||||||
|
x := sl.header
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
for y := x.level[i].forward; y != nil && !item.Less(y.item); y = x.level[i].forward {
|
||||||
|
rank += x.level[i].span
|
||||||
|
x = y
|
||||||
|
}
|
||||||
|
if x.item != nil && !x.item.Less(item) {
|
||||||
|
return rank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *skipList) randomLevel() int {
|
||||||
|
lvl := 1
|
||||||
|
for lvl < sl.maxLevel && float32(sl.random.Uint32()&0xFFFF) < DefaultP*0xFFFF {
|
||||||
|
lvl++
|
||||||
|
}
|
||||||
|
return lvl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds an element by its rank. The rank argument needs to be 1-based.
|
||||||
|
func (sl *skipList) getNodeByRank(rank int) *node {
|
||||||
|
var traversed int
|
||||||
|
x := sl.header
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
for x.level[i].forward != nil && traversed+x.level[i].span <= rank {
|
||||||
|
traversed += x.level[i].span
|
||||||
|
x = x.level[i].forward
|
||||||
|
}
|
||||||
|
if traversed == rank {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *skipList) getMinNode() *node {
|
||||||
|
return sl.header.level[0].forward
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *skipList) getMaxNode() *node {
|
||||||
|
return sl.tail
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the first node greater and the node's 1-based rank.
|
||||||
|
func (sl *skipList) findNext(greater func(i Item) bool) (*node, int) {
|
||||||
|
x := sl.header
|
||||||
|
var rank int
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
for y := x.level[i].forward; y != nil && !greater(y.item); y = x.level[i].forward {
|
||||||
|
rank += x.level[i].span
|
||||||
|
x = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x.level[0].forward, rank + x.level[0].span
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the first node less and the node's 1-based rank.
|
||||||
|
func (sl *skipList) findPrev(less func(i Item) bool) (*node, int) {
|
||||||
|
var rank int
|
||||||
|
x := sl.header
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
for y := x.level[i].forward; y != nil && less(y.item); y = x.level[i].forward {
|
||||||
|
rank += x.level[i].span
|
||||||
|
x = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x, rank
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZSet set
|
||||||
|
type ZSet struct {
|
||||||
|
dict map[string]*node
|
||||||
|
sl *skipList
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new ZSet.
|
||||||
|
func New() *ZSet {
|
||||||
|
return &ZSet{
|
||||||
|
dict: make(map[string]*node),
|
||||||
|
sl: newSkipList(DefaultMaxLevel),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new element or update the score of an existing element. If an item already
|
||||||
|
// exist, the removed item is returned. Otherwise, nil is returned.
|
||||||
|
func (zs *ZSet) Add(key string, item Item) (removeItem Item) {
|
||||||
|
if node := zs.dict[key]; node != nil {
|
||||||
|
// if the node after update, would be still exactly at the same position,
|
||||||
|
// we can just update item.
|
||||||
|
if zs.sl.updateItem(node, item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
removeItem = zs.sl.delete(node)
|
||||||
|
}
|
||||||
|
zs.dict[key] = zs.sl.insert(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the element 'ele' from the sorted set,
|
||||||
|
// return true if the element existed and was deleted, false otherwise
|
||||||
|
func (zs *ZSet) Remove(key string) (removeItem Item) {
|
||||||
|
node := zs.dict[key]
|
||||||
|
if node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
removeItem = zs.sl.delete(node)
|
||||||
|
delete(zs.dict, key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rank return 1-based rank or 0 if not exist
|
||||||
|
func (zs *ZSet) Rank(key string, reverse bool) int {
|
||||||
|
node := zs.dict[key]
|
||||||
|
if node != nil {
|
||||||
|
rank := zs.sl.getRank(node.item)
|
||||||
|
if rank > 0 {
|
||||||
|
if reverse {
|
||||||
|
return zs.sl.length - rank + 1
|
||||||
|
}
|
||||||
|
return rank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zs *ZSet) FindNext(iGreaterThan func(i Item) bool) (v Item, rank int) {
|
||||||
|
n, rank := zs.sl.findNext(iGreaterThan)
|
||||||
|
if n == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return n.item, rank
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zs *ZSet) FindPrev(iLessThan func(i Item) bool) (v Item, rank int) {
|
||||||
|
n, rank := zs.sl.findPrev(iLessThan)
|
||||||
|
if n == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return n.item, rank
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeByScore calls the iterator for every value within the range [min, max],
|
||||||
|
// until iterator return false. If min is nil, it represents negative infinity.
|
||||||
|
// If max is nil, it represents positive infinity.
|
||||||
|
func (zs *ZSet) RangeByScore(min, max func(i Item) bool, reverse bool, iterator ItemIterator) {
|
||||||
|
llen := zs.sl.length
|
||||||
|
var minNode, maxNode *node
|
||||||
|
var minRank, maxRank int
|
||||||
|
if min == nil {
|
||||||
|
minNode = zs.sl.getMinNode()
|
||||||
|
minRank = 1
|
||||||
|
} else {
|
||||||
|
minNode, minRank = zs.sl.findNext(min)
|
||||||
|
}
|
||||||
|
if minNode == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if max == nil {
|
||||||
|
maxNode = zs.sl.getMaxNode()
|
||||||
|
maxRank = llen
|
||||||
|
} else {
|
||||||
|
maxNode, maxRank = zs.sl.findPrev(max)
|
||||||
|
}
|
||||||
|
if maxNode == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if reverse {
|
||||||
|
n := maxNode
|
||||||
|
for i := maxRank; i >= minRank; i-- {
|
||||||
|
if iterator(n.item, llen-i+1) {
|
||||||
|
n = n.backward
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n := minNode
|
||||||
|
for i := minRank; i <= maxRank; i++ {
|
||||||
|
if iterator(n.item, i) {
|
||||||
|
n = n.level[0].forward
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range calls the iterator for every value with in index range [start, end],
|
||||||
|
// until iterator return false. The <start> and <stop> arguments represent
|
||||||
|
// zero-based indexes.
|
||||||
|
func (zs *ZSet) Range(start, end int, reverse bool, iterator ItemIterator) {
|
||||||
|
llen := zs.sl.length
|
||||||
|
if start < 0 {
|
||||||
|
start = llen + start
|
||||||
|
}
|
||||||
|
if end < 0 {
|
||||||
|
end = llen + end
|
||||||
|
}
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
if start > end || start >= llen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if end >= llen {
|
||||||
|
end = llen - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeLen := end - start + 1
|
||||||
|
if reverse {
|
||||||
|
ln := zs.sl.getNodeByRank(llen - start)
|
||||||
|
for i := 1; i <= rangeLen; i++ {
|
||||||
|
if iterator(ln.item, start+i) {
|
||||||
|
ln = ln.backward
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ln := zs.sl.getNodeByRank(start + 1)
|
||||||
|
for i := 1; i <= rangeLen; i++ {
|
||||||
|
if iterator(ln.item, start+i) {
|
||||||
|
ln = ln.level[0].forward
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RangeIterator struct {
|
||||||
|
node *node
|
||||||
|
start, end, cur int
|
||||||
|
reverse bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RangeIterator) Len() int {
|
||||||
|
return r.end - r.start + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RangeIterator) Valid() bool {
|
||||||
|
return r.cur <= r.end
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RangeIterator) Next() {
|
||||||
|
if r.reverse {
|
||||||
|
r.node = r.node.backward
|
||||||
|
} else {
|
||||||
|
r.node = r.node.level[0].forward
|
||||||
|
}
|
||||||
|
r.cur++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RangeIterator) Item() Item {
|
||||||
|
return r.node.item
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RangeIterator) Rank() int {
|
||||||
|
return r.cur + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeIterator return iterator for visit elements in [start, end].
|
||||||
|
// It is slower than Range.
|
||||||
|
func (zs *ZSet) RangeIterator(start, end int, reverse bool) RangeIterator {
|
||||||
|
llen := zs.sl.length
|
||||||
|
if start < 0 {
|
||||||
|
start = llen + start
|
||||||
|
}
|
||||||
|
if end < 0 {
|
||||||
|
end = llen + end
|
||||||
|
}
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if start > end || start >= llen {
|
||||||
|
return RangeIterator{end: -1}
|
||||||
|
}
|
||||||
|
|
||||||
|
if end >= llen {
|
||||||
|
end = llen - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var n *node
|
||||||
|
if reverse {
|
||||||
|
n = zs.sl.getNodeByRank(llen - start)
|
||||||
|
} else {
|
||||||
|
n = zs.sl.getNodeByRank(start + 1)
|
||||||
|
}
|
||||||
|
return RangeIterator{
|
||||||
|
start: start,
|
||||||
|
cur: start,
|
||||||
|
end: end,
|
||||||
|
node: n,
|
||||||
|
reverse: reverse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get return Item in dict.
|
||||||
|
func (zs *ZSet) Get(key string) Item {
|
||||||
|
if node, ok := zs.dict[key]; ok {
|
||||||
|
return node.item
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length return the element count
|
||||||
|
func (zs *ZSet) Length() int {
|
||||||
|
return zs.sl.length
|
||||||
|
}
|
||||||
|
|
||||||
|
type Int int
|
||||||
|
|
||||||
|
func (a Int) Key() string {
|
||||||
|
return strconv.Itoa(int(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Int) Less(b Item) bool {
|
||||||
|
return a < b.(Int)
|
||||||
|
}
|
||||||
529
common/utils/zset/zset_generic.go
Normal file
529
common/utils/zset/zset_generic.go
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
//go:build go1.18
|
||||||
|
|
||||||
|
// Package zset implements sorted set similar to redis zset.
|
||||||
|
package zset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultMaxLevel = 32 // (1/p)^MaxLevel >= maxNode
|
||||||
|
DefaultP = 0.25 // SkipList P = 1/4
|
||||||
|
|
||||||
|
DefaultFreeListSize = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// ItemIterator allows callers of Range* to iterate of the zset.
|
||||||
|
// When this function returns false, iteration will stop.
|
||||||
|
type ItemIterator[T any] func(i T, rank int) bool
|
||||||
|
|
||||||
|
type skipListLevel[T any] struct {
|
||||||
|
forward *node[T]
|
||||||
|
span int
|
||||||
|
}
|
||||||
|
|
||||||
|
// node is an element of a skip list
|
||||||
|
type node[T any] struct {
|
||||||
|
item T
|
||||||
|
backward *node[T]
|
||||||
|
level []skipListLevel[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreeList represents a free list of set node.
|
||||||
|
type FreeList[T any] struct {
|
||||||
|
freelist []*node[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFreeList creates a new free list.
|
||||||
|
func NewFreeList[T any](size int) *FreeList[T] {
|
||||||
|
return &FreeList[T]{freelist: make([]*node[T], 0, size)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FreeList[T]) newNode(lvl int) (n *node[T]) {
|
||||||
|
if len(f.freelist) == 0 {
|
||||||
|
n = new(node[T])
|
||||||
|
n.level = make([]skipListLevel[T], lvl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
index := len(f.freelist) - 1
|
||||||
|
n = f.freelist[index]
|
||||||
|
f.freelist[index] = nil
|
||||||
|
f.freelist = f.freelist[:index]
|
||||||
|
|
||||||
|
if cap(n.level) < lvl {
|
||||||
|
n.level = make([]skipListLevel[T], lvl)
|
||||||
|
} else {
|
||||||
|
n.level = n.level[:lvl]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FreeList[T]) freeNode(n *node[T]) (out bool) {
|
||||||
|
// for gc
|
||||||
|
var zero T
|
||||||
|
n.item = zero
|
||||||
|
for j := 0; j < len(n.level); j++ {
|
||||||
|
n.level[j] = skipListLevel[T]{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(f.freelist) < cap(f.freelist) {
|
||||||
|
f.freelist = append(f.freelist, n)
|
||||||
|
out = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// skipList represents a skip list
|
||||||
|
type skipList[T any] struct {
|
||||||
|
header, tail *node[T]
|
||||||
|
length int
|
||||||
|
level int // current level count
|
||||||
|
maxLevel int
|
||||||
|
freelist *FreeList[T]
|
||||||
|
random *rand.Rand
|
||||||
|
less LessFunc[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSkipList creates a skip list
|
||||||
|
func newSkipList[T any](maxLevel int, less LessFunc[T]) *skipList[T] {
|
||||||
|
if maxLevel < DefaultMaxLevel {
|
||||||
|
panic("maxLevel must < 32")
|
||||||
|
}
|
||||||
|
return &skipList[T]{
|
||||||
|
level: 1,
|
||||||
|
header: &node[T]{
|
||||||
|
level: make([]skipListLevel[T], maxLevel),
|
||||||
|
},
|
||||||
|
maxLevel: maxLevel,
|
||||||
|
freelist: NewFreeList[T](DefaultFreeListSize),
|
||||||
|
random: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
|
less: less,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert an item into the SkipList.
|
||||||
|
func (sl *skipList[T]) insert(item T) *node[T] {
|
||||||
|
var update [DefaultMaxLevel]*node[T] // [0...list.maxLevel)
|
||||||
|
var rank [DefaultMaxLevel]int
|
||||||
|
x := sl.header
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
if i == sl.level-1 {
|
||||||
|
rank[i] = 0
|
||||||
|
} else {
|
||||||
|
rank[i] = rank[i+1]
|
||||||
|
}
|
||||||
|
for y := x.level[i].forward; y != nil && sl.less(y.item, item); y = x.level[i].forward {
|
||||||
|
rank[i] += x.level[i].span
|
||||||
|
x = y
|
||||||
|
}
|
||||||
|
update[i] = x
|
||||||
|
}
|
||||||
|
|
||||||
|
lvl := sl.randomLevel()
|
||||||
|
if lvl > sl.level {
|
||||||
|
for i := sl.level; i < lvl; i++ {
|
||||||
|
rank[i] = 0
|
||||||
|
update[i] = sl.header
|
||||||
|
update[i].level[i].span = sl.length
|
||||||
|
}
|
||||||
|
sl.level = lvl
|
||||||
|
}
|
||||||
|
|
||||||
|
x = sl.freelist.newNode(lvl)
|
||||||
|
x.item = item
|
||||||
|
for i := 0; i < lvl; i++ {
|
||||||
|
x.level[i].forward = update[i].level[i].forward
|
||||||
|
update[i].level[i].forward = x
|
||||||
|
|
||||||
|
x.level[i].span = update[i].level[i].span - (rank[0] - rank[i])
|
||||||
|
update[i].level[i].span = (rank[0] - rank[i]) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment span for untouched levels
|
||||||
|
for i := lvl; i < sl.level; i++ {
|
||||||
|
update[i].level[i].span++
|
||||||
|
}
|
||||||
|
|
||||||
|
if update[0] == sl.header {
|
||||||
|
x.backward = nil
|
||||||
|
} else {
|
||||||
|
x.backward = update[0]
|
||||||
|
}
|
||||||
|
if x.level[0].forward == nil {
|
||||||
|
sl.tail = x
|
||||||
|
} else {
|
||||||
|
x.level[0].forward.backward = x
|
||||||
|
}
|
||||||
|
sl.length++
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete element
|
||||||
|
func (sl *skipList[T]) delete(n *node[T]) (_ T) {
|
||||||
|
var preAlloc [DefaultMaxLevel]*node[T] // [0...list.maxLevel)
|
||||||
|
update := preAlloc[:sl.maxLevel]
|
||||||
|
x := sl.header
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
for y := x.level[i].forward; y != nil && sl.less(y.item, n.item); y = x.level[i].forward {
|
||||||
|
x = y
|
||||||
|
}
|
||||||
|
update[i] = x
|
||||||
|
}
|
||||||
|
x = x.level[0].forward
|
||||||
|
if x != nil && !sl.less(n.item, x.item) {
|
||||||
|
for i := 0; i < sl.level; i++ {
|
||||||
|
if update[i].level[i].forward == x {
|
||||||
|
update[i].level[i].span += x.level[i].span - 1
|
||||||
|
update[i].level[i].forward = x.level[i].forward
|
||||||
|
} else {
|
||||||
|
update[i].level[i].span--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for sl.level > 1 && sl.header.level[sl.level-1].forward == nil {
|
||||||
|
sl.level--
|
||||||
|
}
|
||||||
|
if x.level[0].forward == nil {
|
||||||
|
sl.tail = x.backward
|
||||||
|
} else {
|
||||||
|
x.level[0].forward.backward = x.backward
|
||||||
|
}
|
||||||
|
removeItem := x.item
|
||||||
|
sl.freelist.freeNode(x)
|
||||||
|
sl.length--
|
||||||
|
return removeItem
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *skipList[T]) updateItem(node *node[T], item T) bool {
|
||||||
|
if (node.level[0].forward == nil || !sl.less(node.level[0].forward.item, item)) &&
|
||||||
|
(node.backward == nil || !sl.less(item, node.backward.item)) {
|
||||||
|
node.item = item
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRank find the rank for an element.
|
||||||
|
// Returns 0 when the element cannot be found, rank otherwise.
|
||||||
|
// Note that the rank is 1-based
|
||||||
|
func (sl *skipList[T]) getRank(item T) int {
|
||||||
|
var rank int
|
||||||
|
x := sl.header
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
for y := x.level[i].forward; y != nil && !sl.less(item, y.item); y = x.level[i].forward {
|
||||||
|
rank += x.level[i].span
|
||||||
|
x = y
|
||||||
|
}
|
||||||
|
if x != sl.header && !sl.less(x.item, item) {
|
||||||
|
return rank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *skipList[T]) randomLevel() int {
|
||||||
|
lvl := 1
|
||||||
|
for lvl < sl.maxLevel && float32(sl.random.Uint32()&0xFFFF) < DefaultP*0xFFFF {
|
||||||
|
lvl++
|
||||||
|
}
|
||||||
|
return lvl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds an element by its rank. The rank argument needs to be 1-based.
|
||||||
|
func (sl *skipList[T]) getNodeByRank(rank int) *node[T] {
|
||||||
|
var traversed int
|
||||||
|
x := sl.header
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
for x.level[i].forward != nil && traversed+x.level[i].span <= rank {
|
||||||
|
traversed += x.level[i].span
|
||||||
|
x = x.level[i].forward
|
||||||
|
}
|
||||||
|
if traversed == rank {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *skipList[T]) getMinNode() *node[T] {
|
||||||
|
return sl.header.level[0].forward
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *skipList[T]) getMaxNode() *node[T] {
|
||||||
|
return sl.tail
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the first node greater and the node's 1-based rank.
|
||||||
|
func (sl *skipList[T]) findNext(greater func(i T) bool) (*node[T], int) {
|
||||||
|
x := sl.header
|
||||||
|
var rank int
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
for y := x.level[i].forward; y != nil && !greater(y.item); y = x.level[i].forward {
|
||||||
|
rank += x.level[i].span
|
||||||
|
x = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x.level[0].forward, rank + x.level[0].span
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the first node less and the node's 1-based rank.
|
||||||
|
func (sl *skipList[T]) findPrev(less func(i T) bool) (*node[T], int) {
|
||||||
|
var rank int
|
||||||
|
x := sl.header
|
||||||
|
for i := sl.level - 1; i >= 0; i-- {
|
||||||
|
for y := x.level[i].forward; y != nil && less(y.item); y = x.level[i].forward {
|
||||||
|
rank += x.level[i].span
|
||||||
|
x = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x, rank
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZSet set
|
||||||
|
type ZSet[K comparable, T any] struct {
|
||||||
|
dict map[K]*node[T]
|
||||||
|
sl *skipList[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
// LessFunc determines how to order a type 'T'. It should implement a strict
|
||||||
|
// ordering, and should return true if within that ordering, 'a' < 'b'.
|
||||||
|
type LessFunc[T any] func(a, b T) bool
|
||||||
|
|
||||||
|
// New creates a new ZSet.
|
||||||
|
func New[K comparable, T any](less LessFunc[T]) *ZSet[K, T] {
|
||||||
|
return &ZSet[K, T]{
|
||||||
|
dict: make(map[K]*node[T]),
|
||||||
|
sl: newSkipList[T](DefaultMaxLevel, less),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new element or update the score of an existing element. If an item already
|
||||||
|
// exist, the removed item is returned. Otherwise, nil is returned.
|
||||||
|
func (zs *ZSet[K, T]) Add(key K, item T) (removeItem T) {
|
||||||
|
if node := zs.dict[key]; node != nil {
|
||||||
|
// if the node after update, would be still exactly at the same position,
|
||||||
|
// we can just update item.
|
||||||
|
if zs.sl.updateItem(node, item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
removeItem = zs.sl.delete(node)
|
||||||
|
}
|
||||||
|
zs.dict[key] = zs.sl.insert(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the element 'ele' from the sorted set,
|
||||||
|
// return true if the element existed and was deleted, false otherwise
|
||||||
|
func (zs *ZSet[K, T]) Remove(key K) (removeItem T) {
|
||||||
|
node := zs.dict[key]
|
||||||
|
if node == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
removeItem = zs.sl.delete(node)
|
||||||
|
delete(zs.dict, key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rank return 1-based rank or 0 if not exist
|
||||||
|
func (zs *ZSet[K, T]) Rank(key K, reverse bool) int {
|
||||||
|
node := zs.dict[key]
|
||||||
|
if node != nil {
|
||||||
|
rank := zs.sl.getRank(node.item)
|
||||||
|
if rank > 0 {
|
||||||
|
if reverse {
|
||||||
|
return zs.sl.length - rank + 1
|
||||||
|
}
|
||||||
|
return rank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zs *ZSet[K, T]) FindNext(iGreaterThan func(i T) bool) (v T, rank int) {
|
||||||
|
n, rank := zs.sl.findNext(iGreaterThan)
|
||||||
|
if n == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return n.item, rank
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zs *ZSet[K, T]) FindPrev(iLessThan func(i T) bool) (v T, rank int) {
|
||||||
|
n, rank := zs.sl.findPrev(iLessThan)
|
||||||
|
if n == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return n.item, rank
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeByScore calls the iterator for every value within the range [min, max],
|
||||||
|
// until iterator return false. If min is nil, it represents negative infinity.
|
||||||
|
// If max is nil, it represents positive infinity.
|
||||||
|
func (zs *ZSet[K, T]) RangeByScore(min, max func(i T) bool, reverse bool, iterator ItemIterator[T]) {
|
||||||
|
llen := zs.sl.length
|
||||||
|
var minNode, maxNode *node[T]
|
||||||
|
var minRank, maxRank int
|
||||||
|
if min == nil {
|
||||||
|
minNode = zs.sl.getMinNode()
|
||||||
|
minRank = 1
|
||||||
|
} else {
|
||||||
|
minNode, minRank = zs.sl.findNext(min)
|
||||||
|
}
|
||||||
|
if minNode == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if max == nil {
|
||||||
|
maxNode = zs.sl.getMaxNode()
|
||||||
|
maxRank = llen
|
||||||
|
} else {
|
||||||
|
maxNode, maxRank = zs.sl.findPrev(max)
|
||||||
|
}
|
||||||
|
if maxNode == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if reverse {
|
||||||
|
n := maxNode
|
||||||
|
for i := maxRank; i >= minRank; i-- {
|
||||||
|
if iterator(n.item, llen-i+1) {
|
||||||
|
n = n.backward
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n := minNode
|
||||||
|
for i := minRank; i <= maxRank; i++ {
|
||||||
|
if iterator(n.item, i) {
|
||||||
|
n = n.level[0].forward
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range calls the iterator for every value with in index range [start, end],
|
||||||
|
// until iterator return false. The <start> and <stop> arguments represent
|
||||||
|
// zero-based indexes.
|
||||||
|
func (zs *ZSet[K, T]) Range(start, end int, reverse bool, iterator ItemIterator[T]) {
|
||||||
|
llen := zs.sl.length
|
||||||
|
if start < 0 {
|
||||||
|
start = llen + start
|
||||||
|
}
|
||||||
|
if end < 0 {
|
||||||
|
end = llen + end
|
||||||
|
}
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
if start > end || start >= llen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if end >= llen {
|
||||||
|
end = llen - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeLen := end - start + 1
|
||||||
|
if reverse {
|
||||||
|
ln := zs.sl.getNodeByRank(llen - start)
|
||||||
|
for i := 1; i <= rangeLen; i++ {
|
||||||
|
if iterator(ln.item, start+i) {
|
||||||
|
ln = ln.backward
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ln := zs.sl.getNodeByRank(start + 1)
|
||||||
|
for i := 1; i <= rangeLen; i++ {
|
||||||
|
if iterator(ln.item, start+i) {
|
||||||
|
ln = ln.level[0].forward
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RangeIterator[T any] struct {
|
||||||
|
node *node[T]
|
||||||
|
start, end, cur int
|
||||||
|
reverse bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RangeIterator[T]) Len() int {
|
||||||
|
return r.end - r.start + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RangeIterator[T]) Valid() bool {
|
||||||
|
return r.cur <= r.end
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RangeIterator[T]) Next() {
|
||||||
|
if r.reverse {
|
||||||
|
r.node = r.node.backward
|
||||||
|
} else {
|
||||||
|
r.node = r.node.level[0].forward
|
||||||
|
}
|
||||||
|
r.cur++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RangeIterator[T]) Item() T {
|
||||||
|
return r.node.item
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RangeIterator[T]) Rank() int {
|
||||||
|
return r.cur + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeIterator return iterator for visit elements in [start, end].
|
||||||
|
// It is slower than Range.
|
||||||
|
func (zs *ZSet[K, T]) RangeIterator(start, end int, reverse bool) RangeIterator[T] {
|
||||||
|
llen := zs.sl.length
|
||||||
|
if start < 0 {
|
||||||
|
start = llen + start
|
||||||
|
}
|
||||||
|
if end < 0 {
|
||||||
|
end = llen + end
|
||||||
|
}
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if start > end || start >= llen {
|
||||||
|
return RangeIterator[T]{end: -1}
|
||||||
|
}
|
||||||
|
|
||||||
|
if end >= llen {
|
||||||
|
end = llen - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var n *node[T]
|
||||||
|
if reverse {
|
||||||
|
n = zs.sl.getNodeByRank(llen - start)
|
||||||
|
} else {
|
||||||
|
n = zs.sl.getNodeByRank(start + 1)
|
||||||
|
}
|
||||||
|
return RangeIterator[T]{
|
||||||
|
start: start,
|
||||||
|
cur: start,
|
||||||
|
end: end,
|
||||||
|
node: n,
|
||||||
|
reverse: reverse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get return Item in dict.
|
||||||
|
func (zs *ZSet[K, T]) Get(key K) (item T, found bool) {
|
||||||
|
if n, ok := zs.dict[key]; ok {
|
||||||
|
return n.item, ok
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length return the element count
|
||||||
|
func (zs *ZSet[K, T]) Length() int {
|
||||||
|
return zs.sl.length
|
||||||
|
}
|
||||||
342
common/utils/zset/zset_generic_test.go
Normal file
342
common/utils/zset/zset_generic_test.go
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
//go:build go1.18
|
||||||
|
|
||||||
|
package zset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestRank struct {
|
||||||
|
member string
|
||||||
|
score int
|
||||||
|
}
|
||||||
|
|
||||||
|
// perm returns a random permutation of n Int items in the range [0, n).
|
||||||
|
func perm(n int) (out []TestRank) {
|
||||||
|
out = make([]TestRank, 0, n)
|
||||||
|
for _, v := range rand.Perm(n) {
|
||||||
|
out = append(out, TestRank{
|
||||||
|
member: strconv.Itoa(v),
|
||||||
|
score: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// rang returns an ordered list of Int items in the range [0, n).
|
||||||
|
func rang(n int) (out []TestRank) {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
out = append(out, TestRank{
|
||||||
|
member: strconv.Itoa(i),
|
||||||
|
score: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func revrang(n int, count int) (out []TestRank) {
|
||||||
|
for i := n - 1; i >= n-count; i-- {
|
||||||
|
out = append(out, TestRank{
|
||||||
|
member: strconv.Itoa(i),
|
||||||
|
score: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZSetRank(t *testing.T) {
|
||||||
|
const listSize = 10000
|
||||||
|
zs := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
for _, v := range perm(listSize) {
|
||||||
|
zs.Add(v.member, v)
|
||||||
|
}
|
||||||
|
for _, v := range perm(listSize) {
|
||||||
|
if zs.Rank(v.member, false) != v.score+1 {
|
||||||
|
t.Error("rank error")
|
||||||
|
}
|
||||||
|
if zs.Rank(v.member, true) != listSize-v.score {
|
||||||
|
t.Error("rank error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r []TestRank
|
||||||
|
zs.Range(0, 1, false, func(item TestRank, _ int) bool {
|
||||||
|
r = append(r, item)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !reflect.DeepEqual(r, rang(2)) {
|
||||||
|
t.Error("range error")
|
||||||
|
}
|
||||||
|
|
||||||
|
r = r[:0]
|
||||||
|
zs.RangeByScore(func(i TestRank) bool {
|
||||||
|
return i.score >= 0
|
||||||
|
}, func(i TestRank) bool {
|
||||||
|
return i.score <= 1
|
||||||
|
}, false, func(item TestRank, rank int) bool {
|
||||||
|
r = append(r, item)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !reflect.DeepEqual(r, rang(2)) {
|
||||||
|
t.Error("RangeItem error", r, rang(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
r = r[:0]
|
||||||
|
zs.Range(0, 1, true, func(item TestRank, _ int) bool {
|
||||||
|
r = append(r, item)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !reflect.DeepEqual(r, revrang(listSize, 2)) {
|
||||||
|
t.Error("range error")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < listSize/2; i++ {
|
||||||
|
zs.Remove(strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
for i := listSize + 1; i < listSize; i++ {
|
||||||
|
if r := zs.Rank(strconv.Itoa(i), false); r != i-listSize/2 {
|
||||||
|
t.Error("rank failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRangeItem(t *testing.T) {
|
||||||
|
zs := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
zs.RangeByScore(nil, nil, false, func(i TestRank, rank int) bool {
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, i := range perm(10) {
|
||||||
|
zs.Add(i.member, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r []TestRank
|
||||||
|
zs.RangeByScore(nil, nil, false, func(i TestRank, rank int) bool {
|
||||||
|
r = append(r, i)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !reflect.DeepEqual(r, rang(10)) {
|
||||||
|
t.Error("RangeItem error", r, rang(10))
|
||||||
|
}
|
||||||
|
|
||||||
|
r = r[:0]
|
||||||
|
zs.RangeByScore(func(i TestRank) bool {
|
||||||
|
return i.score >= 3
|
||||||
|
}, func(i TestRank) bool {
|
||||||
|
return i.score <= 5
|
||||||
|
}, false, func(i TestRank, rank int) bool {
|
||||||
|
r = append(r, i)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
var expect []TestRank
|
||||||
|
for i := 3; i <= 5; i++ {
|
||||||
|
expect = append(expect, TestRank{
|
||||||
|
member: strconv.Itoa(i),
|
||||||
|
score: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(r, expect) {
|
||||||
|
t.Error("RangeItem error", r, expect)
|
||||||
|
}
|
||||||
|
|
||||||
|
r = r[:0]
|
||||||
|
zs.RangeByScore(func(i TestRank) bool {
|
||||||
|
return i.score >= 3
|
||||||
|
}, func(i TestRank) bool {
|
||||||
|
return i.score <= 5
|
||||||
|
}, true, func(i TestRank, rank int) bool {
|
||||||
|
r = append(r, i)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect = expect[:0]
|
||||||
|
for i := 5; i >= 3; i-- {
|
||||||
|
expect = append(expect, TestRank{
|
||||||
|
member: strconv.Itoa(i),
|
||||||
|
score: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(r, expect) {
|
||||||
|
t.Error("RangeItem error", r, expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const benchmarkListSize = 10000
|
||||||
|
|
||||||
|
func BenchmarkAdd(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
b.StartTimer()
|
||||||
|
i := 0
|
||||||
|
for i < b.N {
|
||||||
|
tr := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
i++
|
||||||
|
if i >= b.N {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAddIncrease(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := rang(benchmarkListSize)
|
||||||
|
b.StartTimer()
|
||||||
|
i := 0
|
||||||
|
for i < b.N {
|
||||||
|
tr := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
i++
|
||||||
|
if i >= b.N {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAddDecrease(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := revrang(benchmarkListSize, benchmarkListSize)
|
||||||
|
b.StartTimer()
|
||||||
|
i := 0
|
||||||
|
for i < b.N {
|
||||||
|
tr := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
i++
|
||||||
|
if i >= b.N {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRemoveAdd(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
tr := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
tr.Remove(insertP[i%benchmarkListSize].member)
|
||||||
|
item := insertP[i%benchmarkListSize]
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRemove(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
removeP := perm(benchmarkListSize)
|
||||||
|
b.StartTimer()
|
||||||
|
i := 0
|
||||||
|
for i < b.N {
|
||||||
|
b.StopTimer()
|
||||||
|
tr := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
b.StartTimer()
|
||||||
|
for _, item := range removeP {
|
||||||
|
tr.Remove(item.member)
|
||||||
|
i++
|
||||||
|
if i >= b.N {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tr.Length() > 0 {
|
||||||
|
b.Error(tr.Length())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRank(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
tr := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
tr.Rank(insertP[i%benchmarkListSize].member, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRange(b *testing.B) {
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
tr := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
tr.Range(0, 100, true, func(i TestRank, rank int) bool {
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRangeIterator(b *testing.B) {
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
tr := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
it := tr.RangeIterator(0, 100, true)
|
||||||
|
for ; it.Valid(); it.Next() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRangeItem(b *testing.B) {
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
tr := New[string, TestRank](func(a, b TestRank) bool {
|
||||||
|
return a.score < b.score
|
||||||
|
})
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
minScore, maxScore := 0, 100
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
tr.RangeByScore(func(i TestRank) bool {
|
||||||
|
return i.score >= minScore
|
||||||
|
}, func(i TestRank) bool {
|
||||||
|
return i.score <= maxScore
|
||||||
|
}, true, func(i TestRank, rank int) bool {
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
329
common/utils/zset/zset_test.go
Normal file
329
common/utils/zset/zset_test.go
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
//go:build !go1.18
|
||||||
|
// +build !go1.18
|
||||||
|
|
||||||
|
package zset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestRank struct {
|
||||||
|
member string
|
||||||
|
score int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a TestRank) Key() string {
|
||||||
|
return a.member
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a TestRank) Less(than Item) bool {
|
||||||
|
return a.score < than.(TestRank).score
|
||||||
|
}
|
||||||
|
|
||||||
|
// perm returns a random permutation of n Int items in the range [0, n).
|
||||||
|
func perm(n int) (out []TestRank) {
|
||||||
|
out = make([]TestRank, 0, n)
|
||||||
|
for _, v := range rand.Perm(n) {
|
||||||
|
out = append(out, TestRank{
|
||||||
|
member: strconv.Itoa(v),
|
||||||
|
score: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// rang returns an ordered list of Int items in the range [0, n).
|
||||||
|
func rang(n int) (out []TestRank) {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
out = append(out, TestRank{
|
||||||
|
member: strconv.Itoa(i),
|
||||||
|
score: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func revrang(n int, count int) (out []TestRank) {
|
||||||
|
for i := n - 1; i >= n-count; i-- {
|
||||||
|
out = append(out, TestRank{
|
||||||
|
member: strconv.Itoa(i),
|
||||||
|
score: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZSetRank(t *testing.T) {
|
||||||
|
const listSize = 10000
|
||||||
|
zs := New()
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
for _, v := range perm(listSize) {
|
||||||
|
zs.Add(v.member, v)
|
||||||
|
}
|
||||||
|
for _, v := range perm(listSize) {
|
||||||
|
if zs.Rank(v.Key(), false) != v.score+1 {
|
||||||
|
t.Error("rank error")
|
||||||
|
}
|
||||||
|
if zs.Rank(v.Key(), true) != listSize-v.score {
|
||||||
|
t.Error("rank error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r []Item
|
||||||
|
zs.Range(0, 1, false, func(item Item, _ int) bool {
|
||||||
|
r = append(r, item)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !reflect.DeepEqual(r, rang(2)) {
|
||||||
|
t.Error("range error")
|
||||||
|
}
|
||||||
|
|
||||||
|
r = r[:0]
|
||||||
|
zs.RangeByScore(func(i Item) bool {
|
||||||
|
return i.(TestRank).score >= 0
|
||||||
|
}, func(i Item) bool {
|
||||||
|
return i.(TestRank).score <= 1
|
||||||
|
}, false, func(item Item, rank int) bool {
|
||||||
|
r = append(r, item)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !reflect.DeepEqual(r, rang(2)) {
|
||||||
|
t.Error("RangeItem error", r, rang(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
r = r[:0]
|
||||||
|
zs.Range(0, 1, true, func(item Item, _ int) bool {
|
||||||
|
r = append(r, item)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !reflect.DeepEqual(r, revrang(listSize, 2)) {
|
||||||
|
t.Error("range error")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < listSize/2; i++ {
|
||||||
|
zs.Remove(strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
for i := listSize + 1; i < listSize; i++ {
|
||||||
|
if r := zs.Rank(strconv.Itoa(i), false); r != i-listSize/2 {
|
||||||
|
t.Error("rank failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRangeItem(t *testing.T) {
|
||||||
|
zs := New()
|
||||||
|
zs.RangeByScore(nil, nil, false, func(i Item, rank int) bool {
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, i := range perm(10) {
|
||||||
|
zs.Add(i.member, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r []Item
|
||||||
|
zs.RangeByScore(nil, nil, false, func(i Item, rank int) bool {
|
||||||
|
r = append(r, i)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !reflect.DeepEqual(r, rang(10)) {
|
||||||
|
t.Error("RangeItem error", r, rang(10))
|
||||||
|
}
|
||||||
|
|
||||||
|
r = r[:0]
|
||||||
|
zs.RangeByScore(func(i Item) bool {
|
||||||
|
return i.(TestRank).score >= 3
|
||||||
|
}, func(i Item) bool {
|
||||||
|
return i.(TestRank).score <= 5
|
||||||
|
}, false, func(i Item, rank int) bool {
|
||||||
|
r = append(r, i)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
var expect []Item
|
||||||
|
for i := 3; i <= 5; i++ {
|
||||||
|
expect = append(expect, TestRank{
|
||||||
|
member: strconv.Itoa(i),
|
||||||
|
score: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(r, expect) {
|
||||||
|
t.Error("RangeItem error", r, expect)
|
||||||
|
}
|
||||||
|
|
||||||
|
r = r[:0]
|
||||||
|
zs.RangeByScore(func(i Item) bool {
|
||||||
|
return i.(TestRank).score >= 3
|
||||||
|
}, func(i Item) bool {
|
||||||
|
return i.(TestRank).score <= 5
|
||||||
|
}, true, func(i Item, rank int) bool {
|
||||||
|
r = append(r, i)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect = expect[:0]
|
||||||
|
for i := 5; i >= 3; i-- {
|
||||||
|
expect = append(expect, TestRank{
|
||||||
|
member: strconv.Itoa(i),
|
||||||
|
score: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(r, expect) {
|
||||||
|
t.Error("RangeItem error", r, expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const benchmarkListSize = 10000
|
||||||
|
|
||||||
|
func BenchmarkAdd(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
b.StartTimer()
|
||||||
|
i := 0
|
||||||
|
for i < b.N {
|
||||||
|
tr := New()
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
i++
|
||||||
|
if i >= b.N {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAddIncrease(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := rang(benchmarkListSize)
|
||||||
|
b.StartTimer()
|
||||||
|
i := 0
|
||||||
|
for i < b.N {
|
||||||
|
tr := New()
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.Key(), item)
|
||||||
|
i++
|
||||||
|
if i >= b.N {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAddDecrease(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := revrang(benchmarkListSize, benchmarkListSize)
|
||||||
|
b.StartTimer()
|
||||||
|
i := 0
|
||||||
|
for i < b.N {
|
||||||
|
tr := New()
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
i++
|
||||||
|
if i >= b.N {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRemoveAdd(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
tr := New()
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
tr.Remove(insertP[i%benchmarkListSize].Key())
|
||||||
|
item := insertP[i%benchmarkListSize]
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRemove(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
removeP := perm(benchmarkListSize)
|
||||||
|
b.StartTimer()
|
||||||
|
i := 0
|
||||||
|
for i < b.N {
|
||||||
|
b.StopTimer()
|
||||||
|
tr := New()
|
||||||
|
for _, v := range insertP {
|
||||||
|
tr.Add(v.member, v)
|
||||||
|
}
|
||||||
|
b.StartTimer()
|
||||||
|
for _, item := range removeP {
|
||||||
|
tr.Remove(item.Key())
|
||||||
|
i++
|
||||||
|
if i >= b.N {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tr.Length() > 0 {
|
||||||
|
b.Error(tr.Length())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRank(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
tr := New()
|
||||||
|
for _, v := range insertP {
|
||||||
|
tr.Add(v.member, v)
|
||||||
|
}
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
tr.Rank(insertP[i%benchmarkListSize].Key(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRange(b *testing.B) {
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
tr := New()
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
tr.Range(0, 100, true, func(i Item, rank int) bool {
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRangeIterator(b *testing.B) {
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
tr := New()
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
it := tr.RangeIterator(0, 100, true)
|
||||||
|
for ; it.Valid(); it.Next() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRangeItem(b *testing.B) {
|
||||||
|
insertP := perm(benchmarkListSize)
|
||||||
|
tr := New()
|
||||||
|
for _, item := range insertP {
|
||||||
|
tr.Add(item.member, item)
|
||||||
|
}
|
||||||
|
minScore, maxScore := 0, 100
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
tr.RangeByScore(func(i Item) bool {
|
||||||
|
return i.(TestRank).score >= minScore
|
||||||
|
}, func(i Item) bool {
|
||||||
|
return i.(TestRank).score <= maxScore
|
||||||
|
}, true, func(i Item, rank int) bool {
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
183
docs/boss-script-hookaction-guide-2026-04-05.md
Normal file
183
docs/boss-script-hookaction-guide-2026-04-05.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# Boss Script(HookAction)接入说明
|
||||||
|
|
||||||
|
日期:2026-04-05
|
||||||
|
|
||||||
|
## 1. 执行流程
|
||||||
|
|
||||||
|
1. 先执行战斗效果链 `HookAction()`
|
||||||
|
2. 执行脚本 `hookAction(hookaction)`
|
||||||
|
3. 用脚本返回值决定是否继续出手
|
||||||
|
4. 脚本可直接调用 Go 绑定函数:`useSkill()`、`switchPet()`
|
||||||
|
|
||||||
|
## 2. JS 可调用的 Go 函数
|
||||||
|
|
||||||
|
1. `useSkill(skillId: number)`
|
||||||
|
2. `switchPet(catchTime: number)`
|
||||||
|
|
||||||
|
## 3. `hookaction` 参数字段
|
||||||
|
|
||||||
|
基础字段:
|
||||||
|
|
||||||
|
1. `hookaction.hookaction: boolean`
|
||||||
|
2. `hookaction.round: number`
|
||||||
|
3. `hookaction.is_first: boolean`
|
||||||
|
4. `hookaction.our: { pet_id, catch_time, hp, max_hp } | null`
|
||||||
|
5. `hookaction.opp: { pet_id, catch_time, hp, max_hp } | null`
|
||||||
|
6. `hookaction.skills: Array<{ skill_id, pp, can_use }>`
|
||||||
|
|
||||||
|
AttackValue 映射字段(重点):
|
||||||
|
|
||||||
|
1. `hookaction.our_attack`
|
||||||
|
2. `hookaction.opp_attack`
|
||||||
|
|
||||||
|
结构:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
skill_id: number;
|
||||||
|
attack_time: number;
|
||||||
|
is_critical: number;
|
||||||
|
lost_hp: number;
|
||||||
|
gain_hp: number;
|
||||||
|
remain_hp: number;
|
||||||
|
max_hp: number;
|
||||||
|
state: number;
|
||||||
|
offensive: number;
|
||||||
|
status: number[]; // 对应 AttackValue.Status[20]
|
||||||
|
prop: number[]; // 对应 AttackValue.Prop[6]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
- `prop` 索引:`[攻, 防, 特攻, 特防, 速度, 命中]`
|
||||||
|
- 对应值 `> 0` 代表强化,`< 0` 代表下降,`0` 代表无变化
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
- `true`:继续行动
|
||||||
|
- `false`:阻止行动
|
||||||
|
- 不返回:默认回退到 `hookaction.hookaction`
|
||||||
|
|
||||||
|
## 4. 脚本示例
|
||||||
|
|
||||||
|
### 4.1 判断对方是否存在强化(你问的这个)
|
||||||
|
|
||||||
|
```js
|
||||||
|
function hookAction(hookaction) {
|
||||||
|
if (!hookaction.hookaction) return false;
|
||||||
|
|
||||||
|
var oppAtk = hookaction.opp_attack;
|
||||||
|
var oppHasBuff = false;
|
||||||
|
if (oppAtk && oppAtk.prop) {
|
||||||
|
for (var i = 0; i < oppAtk.prop.length; i++) {
|
||||||
|
if (oppAtk.prop[i] > 0) {
|
||||||
|
oppHasBuff = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oppHasBuff) {
|
||||||
|
// 对方有强化时,放一个针对技能
|
||||||
|
useSkill(5001);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 判断对方是否有异常状态
|
||||||
|
|
||||||
|
```js
|
||||||
|
function hookAction(hookaction) {
|
||||||
|
if (!hookaction.hookaction) return false;
|
||||||
|
|
||||||
|
var oppAtk = hookaction.opp_attack;
|
||||||
|
var hasStatus = false;
|
||||||
|
if (oppAtk && oppAtk.status) {
|
||||||
|
for (var i = 0; i < oppAtk.status.length; i++) {
|
||||||
|
if (oppAtk.status[i] > 0) {
|
||||||
|
hasStatus = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasStatus) {
|
||||||
|
// 没有异常时尝试上异常
|
||||||
|
useSkill(6002);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Monaco 类型提示
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import * as monaco from "monaco-editor";
|
||||||
|
|
||||||
|
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||||
|
allowNonTsExtensions: true,
|
||||||
|
checkJs: true,
|
||||||
|
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||||
|
});
|
||||||
|
|
||||||
|
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||||
|
`
|
||||||
|
interface BossHookPetContext {
|
||||||
|
pet_id: number;
|
||||||
|
catch_time: number;
|
||||||
|
hp: number;
|
||||||
|
max_hp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BossHookSkillContext {
|
||||||
|
skill_id: number;
|
||||||
|
pp: number;
|
||||||
|
can_use: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BossHookAttackContext {
|
||||||
|
skill_id: number;
|
||||||
|
attack_time: number;
|
||||||
|
is_critical: number;
|
||||||
|
lost_hp: number;
|
||||||
|
gain_hp: number;
|
||||||
|
remain_hp: number;
|
||||||
|
max_hp: number;
|
||||||
|
state: number;
|
||||||
|
offensive: number;
|
||||||
|
status: number[];
|
||||||
|
prop: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BossHookActionContext {
|
||||||
|
hookaction: boolean;
|
||||||
|
round: number;
|
||||||
|
is_first: boolean;
|
||||||
|
our: BossHookPetContext | null;
|
||||||
|
opp: BossHookPetContext | null;
|
||||||
|
skills: BossHookSkillContext[];
|
||||||
|
our_attack: BossHookAttackContext | null;
|
||||||
|
opp_attack: BossHookAttackContext | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare function hookAction(hookaction: BossHookActionContext): boolean;
|
||||||
|
declare function HookAction(hookaction: BossHookActionContext): boolean;
|
||||||
|
declare function hookaction(hookaction: BossHookActionContext): boolean;
|
||||||
|
|
||||||
|
declare function useSkill(skillId: number): void;
|
||||||
|
declare function switchPet(catchTime: number): void;
|
||||||
|
`,
|
||||||
|
"ts:boss-script.d.ts"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 后端代码
|
||||||
|
|
||||||
|
- 脚本执行器与函数绑定:`modules/config/model/boss_pet.go`
|
||||||
|
- AI 出手转发与上下文构建:`logic/service/fight/input/ai.go`
|
||||||
|
|
||||||
400
docs/fight-group-implementation-checklist-2026-04-04.md
Normal file
400
docs/fight-group-implementation-checklist-2026-04-04.md
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# 战斗系统对齐 `flash/group` 组队战斗实施清单(执行版)
|
||||||
|
|
||||||
|
日期:2026-04-04
|
||||||
|
适用仓库:`E:\newcode\sun`
|
||||||
|
参考客户端仓库:`E:\newcode\flash`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 结论与范围
|
||||||
|
|
||||||
|
### 1.1 结论
|
||||||
|
|
||||||
|
- `sun` 当前战斗系统具备多战位骨架(`ActorIndex/TargetIndex`、`Our/Opp []*input.Input`),但未完成组队战斗全链路。
|
||||||
|
- `flash` 的 `group` 分支当前 HEAD 已回滚组队重构;组队实现主要存在于历史提交 `4c07fa07`。
|
||||||
|
- 因此本次不是“直接搬代码”,而是“按协议与行为对齐实现”。
|
||||||
|
|
||||||
|
### 1.2 本清单目标
|
||||||
|
|
||||||
|
- 在不破坏现有 `1v1` 的前提下,落地组队战斗可运行版本(MVP)。
|
||||||
|
- 对齐 `flash`/社区实现中的关键行为(开战、出招、切宠、道具、结算、战斗结束)。
|
||||||
|
- 协议层采用“一个统一结构体 + phase 字段”方案,单打/双打共用同一序列化模型。
|
||||||
|
- 保留旧 `24xx/25xx` 流程入口,通过服务端适配映射到统一结构体。
|
||||||
|
|
||||||
|
### 1.3 非目标
|
||||||
|
|
||||||
|
- 不要求一次性 100% 复刻客户端所有 UI/演出细节。
|
||||||
|
- 不要求一次性改完全部 effect;先保证核心流程可跑,再分批清理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 基线事实(实施前必须统一认知)
|
||||||
|
|
||||||
|
### 2.1 `flash` 仓库事实
|
||||||
|
|
||||||
|
- `group` 分支相对 `main` 的提交:
|
||||||
|
- `4c07fa07 refactor(group-fight)`(引入组队)
|
||||||
|
- `a410bfca Revert "refactor(group-fight)"`(回滚组队)
|
||||||
|
- `e2382a4f`(地图重构)
|
||||||
|
- `bd84f206`(.gitignore)
|
||||||
|
- 所以 `group` HEAD 不再包含 `GroupFightDLL`、`core/group/*` 组队代码;需参考 `4c07fa07` 的内容。
|
||||||
|
|
||||||
|
### 2.2 `sun` 战斗现状
|
||||||
|
|
||||||
|
- 已有多战位骨架:
|
||||||
|
- `logic/service/fight/input.go`:`Our/Opp []*input.Input`
|
||||||
|
- `logic/service/fight/action/BattleAction.go`:`ActorIndex/TargetIndex`
|
||||||
|
- `logic/service/fight/new_options.go`:`WithFightPlayersOnSide/WithFightInputs`
|
||||||
|
- 仍有关键缺口:
|
||||||
|
- 控制器入站仍是单战位参数(如 `2405/2406/2407` 只传技能/道具/catchTime)
|
||||||
|
- 回合主链仍以双动作兼容流程为中心
|
||||||
|
- 组队相关特性存在 TODO(例如 `501/502/503`)
|
||||||
|
|
||||||
|
### 2.3 外部实现参考(本次新增)
|
||||||
|
|
||||||
|
- `arcadia-star/seer2-fight-ui`
|
||||||
|
- 双打核心模型不是独立命令集,而是统一帧模型 + `uiStyle + side + position`。
|
||||||
|
- `uiStyle` 支持 `2v2/2v1`,战位通过 `position(main/sub)` 区分。
|
||||||
|
- `arcadia-star/seer2-next-message/src/entity/fight.rs`
|
||||||
|
- 采用统一战斗实体结构:`team/user/pet` + `side/position`。
|
||||||
|
- 行为包拆分为 `Load/Hurt/Change/Escape/...`,但底层字段模型统一。
|
||||||
|
- `ukuq/seer2-server/src/seer2/fight`
|
||||||
|
- `ArenaResourceLoadCMD -> TeamInfo -> FightUserInfo -> FighterInfo` 为层级化统一结构。
|
||||||
|
- `FighterInfo` 直接包含 `position/hp/maxHp/anger/skills`,适合直接映射为本项目统一结构体。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 协议对齐清单(按优先级)
|
||||||
|
|
||||||
|
> 说明:本清单改为“统一协议结构体”路线,不再强制先实现 `75xx` 独立命令族。
|
||||||
|
> 推荐做法:保留旧入口命令,服务端内部统一转为 `FightActionEnvelope/FightStateEnvelope`。
|
||||||
|
|
||||||
|
### 3.1 P0 必做(MVP 必须)
|
||||||
|
|
||||||
|
- [ ] 统一入站动作结构 `FightActionEnvelope`
|
||||||
|
- 最少字段:`actionType/actorIndex/targetIndex/skillId/itemId/catchTime/escape/chat`
|
||||||
|
- 兼容映射:
|
||||||
|
- `2405 -> actionType=skill`
|
||||||
|
- `2406 -> actionType=item`
|
||||||
|
- `2407 -> actionType=change`
|
||||||
|
- `2410 -> actionType=escape`
|
||||||
|
- [ ] 统一出站状态结构 `FightStateEnvelope`
|
||||||
|
- 最少字段:
|
||||||
|
- `phase`(`start/skill_hurt/change/over/load/chat`)
|
||||||
|
- `left[]/right[]`(元素为统一 `FighterState`)
|
||||||
|
- `meta`(回合号、天气、胜负、结束原因)
|
||||||
|
- [ ] 统一战位子结构 `FighterState`
|
||||||
|
- 每项至少包含:`side/position(userSlot)/userId/petId(catchTime)/hp/maxHp/level/anger/status/prop/skills`
|
||||||
|
|
||||||
|
### 3.2 P1 强烈建议(提升一致性)
|
||||||
|
|
||||||
|
- [ ] 完善 `phase=skill_hurt`
|
||||||
|
- 至少带:施法方快照、受击方快照、技能、暴击、伤害、HP 变更
|
||||||
|
- [ ] 完善 `phase=change`
|
||||||
|
- 至少带:切宠发起位、切入目标位、新精灵状态
|
||||||
|
- [ ] 完善 `phase=over`
|
||||||
|
- 至少带:结束原因、胜方、收益主体
|
||||||
|
- [ ] 完善 `phase=load/chat`
|
||||||
|
- 组队加载进度、战斗内聊天统一走同一 envelope
|
||||||
|
|
||||||
|
### 3.3 P2 视时间补齐
|
||||||
|
|
||||||
|
- [ ] `phase=sprite_die/sprite_notice/win_close`
|
||||||
|
- [ ] `phase=skill_wait/skill_wait_notice`
|
||||||
|
- [ ] `phase=overtime/timeout_exit/relation_notice`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 代码改造任务清单(可直接分工)
|
||||||
|
|
||||||
|
## 4.1 协议与结构层(Owner A)
|
||||||
|
|
||||||
|
- [ ] 新增统一协议结构文件
|
||||||
|
- 建议新建:`logic/service/fight/cmd_unified.go`
|
||||||
|
- 要求:统一定义 `FightActionEnvelope` 和映射辅助结构
|
||||||
|
|
||||||
|
- [ ] 新增统一出站结构
|
||||||
|
- 建议新建:`logic/service/fight/info/unified_info.go`
|
||||||
|
- 要求:定义 `FightStateEnvelope/FighterState`,支持单打与双打
|
||||||
|
|
||||||
|
- [ ] 统一战位字段命名规范
|
||||||
|
- `actorIndex`:我方执行位
|
||||||
|
- `targetIndex`:敌方目标位
|
||||||
|
- `side+pos` 与 `actorIndex/targetIndex` 转换规则写入注释
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
- [ ] 旧 cmd(`2405/2406/2407/2410`)可无损映射到统一入站结构。
|
||||||
|
- [ ] 统一出站结构在 `start/skill_hurt/change/over` phase 均可序列化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.2 控制器与路由层(Owner B)
|
||||||
|
|
||||||
|
- [ ] 新增统一动作入口(可单文件)
|
||||||
|
- 建议新建:`logic/controller/fight_unified.go`
|
||||||
|
- 用途:将旧包和未来扩展包统一落到 `FightActionEnvelope`
|
||||||
|
|
||||||
|
- [ ] 兼容旧协议入口
|
||||||
|
- `2405/2406/2407` 保持可用(默认 `actorIndex=0,targetIndex=0`)
|
||||||
|
- 组队场景由 `actorIndex/targetIndex` 与战斗上下文决定,不再依赖独立 `75xx`
|
||||||
|
|
||||||
|
- [ ] 增加战前校验
|
||||||
|
- 成员是否在同一组队房间
|
||||||
|
- 战斗状态互斥
|
||||||
|
- 战位可操作权限
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
- [ ] 任意技能动作都能转化为 `UseSkillAt(...)`(含 `actorIndex/targetIndex`)。
|
||||||
|
- [ ] 非法战位命令被拒绝,不影响其他战位。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.3 战斗核心层(Owner C)
|
||||||
|
|
||||||
|
- [ ] 固化“多动作一回合”模型
|
||||||
|
- `collectPlayerActions`:按预期战位数收集,不是按两人收集
|
||||||
|
- `resolveRound`:每回合一次统一排序与执行
|
||||||
|
|
||||||
|
- [ ] 降低对“双动作 enterturn”的耦合
|
||||||
|
- 当前 `enterturn(first, second)` 作为兼容层保留
|
||||||
|
- 新逻辑要确保:
|
||||||
|
- 回合开始钩子只执行一次/回合
|
||||||
|
- 回合结束钩子只执行一次/回合
|
||||||
|
- 不因 pair 分片导致重复触发
|
||||||
|
|
||||||
|
- [ ] 完善动作-战位映射
|
||||||
|
- `GetInputByAction` 在组队模式下严格按 `playerID + actorIndex/targetIndex` 定位
|
||||||
|
- 超时补默认动作按战位补齐
|
||||||
|
|
||||||
|
- [ ] 完善死亡换宠/主动换宠
|
||||||
|
- 按 actorIndex 粒度处理
|
||||||
|
- 切宠广播必须携带 actor 位信息
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
- [ ] 2v2 场景一回合四动作都参与排序,不丢动作。
|
||||||
|
- [ ] 同玩家多战位动作不会互相覆盖。
|
||||||
|
- [ ] 任一战位死亡只影响对应战位换宠链路。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.4 组队战报与广播层(Owner D)
|
||||||
|
|
||||||
|
- [ ] 统一战报快照结构
|
||||||
|
- 至少包含:
|
||||||
|
- 施法方:`userId/actorIndex/skillId/crit/dmg/hpAfter/status/prop`
|
||||||
|
- 受击方:`userId/actorIndex/hpAfter/status/prop`
|
||||||
|
|
||||||
|
- [ ] 完成关键广播
|
||||||
|
- 开战广播
|
||||||
|
- 技能结果广播
|
||||||
|
- 切宠成功广播
|
||||||
|
- 战斗结束广播
|
||||||
|
|
||||||
|
- [ ] 保留旧包兼容(必要时双发)
|
||||||
|
- 单打/双打统一走同一结构体
|
||||||
|
- 如前端未升级,可按需保留旧 `2503/2505/2506` 过渡映射
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
- [ ] 观战端/队友端收到的战位与 HP 同步一致。
|
||||||
|
- [ ] 切宠后不会出现“错位显示”。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.5 Effect 与规则层(Owner E)
|
||||||
|
|
||||||
|
- [ ] 先补明确组队依赖效果
|
||||||
|
- `logic/service/fight/boss/NewSeIdx_501.go`
|
||||||
|
- `logic/service/fight/boss/NewSeIdx_502.go`
|
||||||
|
- `logic/service/fight/boss/NewSeIdx_503.go`
|
||||||
|
|
||||||
|
- [ ] 统一“队友”查询工具函数
|
||||||
|
- 建议在 `input` 或 `fight` 层提供:
|
||||||
|
- 获取同阵营存活战位
|
||||||
|
- 获取队友列表(排除自己)
|
||||||
|
- 群体目标选择上限
|
||||||
|
|
||||||
|
- [ ] 扫描组队敏感 effect
|
||||||
|
- 特别关注含“组队对战时无效”描述项(如 effect 457)
|
||||||
|
- 明确:是直接禁用,还是按组队模式替代逻辑
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
- [ ] `501/502/503` 在 2v2 场景行为符合设计。
|
||||||
|
- [ ] 组队模式下不再出现空指针或越界。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.6 测试与回归(Owner F)
|
||||||
|
|
||||||
|
- [ ] 单测补齐
|
||||||
|
- `logic/service/fight/action_test.go`:继续扩充多战位覆盖
|
||||||
|
- 新增建议:
|
||||||
|
- `logic/service/fight/loop_multi_test.go`
|
||||||
|
- `logic/service/fight/fight_group_test.go`
|
||||||
|
|
||||||
|
- [ ] 集成回归用例(最少)
|
||||||
|
- Case 1:1v1 旧流程
|
||||||
|
- Case 2:2v2 双方四动作
|
||||||
|
- Case 3:同一玩家两战位各自出招
|
||||||
|
- Case 4:中途切宠 + 被动死亡切宠
|
||||||
|
- Case 5:超时默认动作补齐
|
||||||
|
- Case 6:逃跑/掉线结束
|
||||||
|
|
||||||
|
- [ ] 构建与测试命令
|
||||||
|
- `cd logic && go test ./service/fight/...`
|
||||||
|
- `cd logic && go test ./controller/...`
|
||||||
|
- `cd logic && go build ./...`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 文件级任务地图(便于派工)
|
||||||
|
|
||||||
|
- 协议/结构:
|
||||||
|
- `logic/service/fight/cmd.go`
|
||||||
|
- `logic/service/fight/cmd_unified.go`(新增)
|
||||||
|
- `logic/service/fight/info/info.go`
|
||||||
|
- `logic/service/fight/info/unified_info.go`(新增)
|
||||||
|
|
||||||
|
- 控制器:
|
||||||
|
- `logic/controller/fight_base.go`
|
||||||
|
- `logic/controller/fight_pvp_withplayer.go`
|
||||||
|
- `logic/controller/fight_unified.go`(新增)
|
||||||
|
|
||||||
|
- 核心流程:
|
||||||
|
- `logic/service/fight/new.go`
|
||||||
|
- `logic/service/fight/new_options.go`
|
||||||
|
- `logic/service/fight/input.go`
|
||||||
|
- `logic/service/fight/action.go`
|
||||||
|
- `logic/service/fight/loop.go`
|
||||||
|
- `logic/service/fight/fightc.go`
|
||||||
|
|
||||||
|
- Effect:
|
||||||
|
- `logic/service/fight/boss/NewSeIdx_501.go`
|
||||||
|
- `logic/service/fight/boss/NewSeIdx_502.go`
|
||||||
|
- `logic/service/fight/boss/NewSeIdx_503.go`
|
||||||
|
- 其他含组队语义的 effect 文件
|
||||||
|
|
||||||
|
- 测试:
|
||||||
|
- `logic/service/fight/action_test.go`
|
||||||
|
- `logic/service/fight/*_test.go`(新增)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 里程碑与交付标准
|
||||||
|
|
||||||
|
### M1(协议可通)
|
||||||
|
|
||||||
|
- [ ] 统一结构体可完成 `start/skill_hurt/change/over` 四类下发
|
||||||
|
- [ ] 旧命令入口均可映射到 `FightC` indexed 接口
|
||||||
|
|
||||||
|
### M2(核心可跑)
|
||||||
|
|
||||||
|
- [ ] 2v2 全回合可稳定执行
|
||||||
|
- [ ] 切宠/道具/超时可用
|
||||||
|
|
||||||
|
### M3(规则可用)
|
||||||
|
|
||||||
|
- [ ] 501/502/503 完成
|
||||||
|
- [ ] 主要组队战报可用
|
||||||
|
|
||||||
|
### M4(回归上线)
|
||||||
|
|
||||||
|
- [ ] 1v1 不回归
|
||||||
|
- [ ] `go test` 与 `go build` 通过
|
||||||
|
- [ ] 文档补充已完成项与遗留项
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 风险清单与缓解
|
||||||
|
|
||||||
|
- 风险:旧逻辑大量默认 `CurPet[0]`,多人战位容易错位。
|
||||||
|
缓解:引入统一 `CurrentPetByActor`/`TargetByIndex` 访问函数,禁止新代码直接写死 `[0]`。
|
||||||
|
|
||||||
|
- 风险:`enterturn` 兼容层导致钩子重复触发。
|
||||||
|
缓解:把“回合开始/结束”从 pair 执行中抽离,确保每回合只触发一次。
|
||||||
|
|
||||||
|
- 风险:协议切换导致旧客户端不可用。
|
||||||
|
缓解:服务端保持旧入口不变,先做“旧包 -> 统一结构”映射;前端按版本切流。
|
||||||
|
|
||||||
|
- 风险:effect 批量改动引发回归。
|
||||||
|
缓解:先做组队关键 effect,其他 effect 分批迁移并每批回归。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 实施顺序建议(最小阻塞)
|
||||||
|
|
||||||
|
1. 协议结构与控制器入口
|
||||||
|
2. 动作收集与回合统一执行
|
||||||
|
3. 切宠/道具/超时按战位修正
|
||||||
|
4. 关键广播与战报
|
||||||
|
5. 组队 effect(501/502/503)
|
||||||
|
6. 全量测试与回归
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 交接要求(给执行同学)
|
||||||
|
|
||||||
|
- 每完成一个里程碑,在 `docs/` 新增一段“完成项/未完成项/阻塞项”。
|
||||||
|
- 如改动协议字段,必须附抓包样例或字段注释,不允许只改代码不补说明。
|
||||||
|
- 如发现与本清单冲突的历史逻辑,以“兼容线上行为优先”,并在文档记录偏差原因。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 可实现性结论(统一协议结构体)
|
||||||
|
|
||||||
|
- 结论:可实现,且风险可控。
|
||||||
|
- 依据:
|
||||||
|
- `seer2-fight-ui` 的双打模型本质是统一数据结构 + `uiStyle/side/position`,不是强依赖独立命令族。
|
||||||
|
- `seer2-next-message` 与 `seer2-server` 都采用统一 `team/user/pet` 层级结构,`position` 作为战位核心字段。
|
||||||
|
- 本仓库已具备 `actorIndex/targetIndex` 与 `UseSkillAt/ChangePetAt/UseItemAt` 能力,协议统一后只需补齐映射和广播。
|
||||||
|
- 实施建议:
|
||||||
|
- 先完成“旧入口 -> 统一入站结构”映射。
|
||||||
|
- 再完成“统一出站结构 + phase 广播”。
|
||||||
|
- 最后做前端切换与旧包退场(或长期双通道兼容)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AtkType 目标语义补充(2026-04-05)
|
||||||
|
|
||||||
|
来源:`flash` 端 `SkillXMLInfo.getGpFtSkillType(skillID)`,读取 `movesMap/moveStoneMap` 的 `AtkType`。
|
||||||
|
|
||||||
|
GBTL 规则(已确认):
|
||||||
|
|
||||||
|
1. `AtkNum`:本技能同时攻击数量,默认 `1`(不能为 `0`)
|
||||||
|
2. `AtkType`:目标范围
|
||||||
|
- `0`:所有人
|
||||||
|
- `1`:仅己方
|
||||||
|
- `2`:仅对方
|
||||||
|
- `3`:仅自己
|
||||||
|
- 默认:`2`
|
||||||
|
|
||||||
|
前端目标选择行为(`SkillMouseController.attack(skillID, attackType)`):
|
||||||
|
|
||||||
|
1. `attackType=0` -> `allPetWinList`(全体可选)
|
||||||
|
2. `attackType=1` -> `membPetWinList`(己方可选,含自己与队友)
|
||||||
|
3. `attackType=2` -> `oppPetWinList`(敌方可选)
|
||||||
|
4. `attackType=3` -> `[playerMode.petWin]`(仅自己)
|
||||||
|
|
||||||
|
后端目标关系判定(组队/多战位必须遵循):
|
||||||
|
|
||||||
|
1. 若协议传 `actor + target(side,pos)`:
|
||||||
|
- `target.side != actor.side` => 对方目标
|
||||||
|
- `target.side == actor.side && target.pos == actor.pos` => 自身目标
|
||||||
|
- `target.side == actor.side && target.pos != actor.pos` => 队友目标
|
||||||
|
2. 若协议未显式传目标(旧 `2405`):
|
||||||
|
- 用 `AtkType` 兜底:
|
||||||
|
- `AtkType=3` => 强制自身
|
||||||
|
- `AtkType=1` => 默认自身(无显式队友位时)
|
||||||
|
- 其他 => 维持旧行为(默认对方 `0` 位)
|
||||||
|
|
||||||
|
实施要求(与现有清单并行):
|
||||||
|
|
||||||
|
1. `common/data/xmlres/skill.go` 的 `Move` 需包含 `AtkType` 字段解析。
|
||||||
|
2. 动作目标不再依赖“默认 Opp 绑定”;effect 上下文必须使用“本次动作的实际目标”。
|
||||||
|
3. 需支持区分 `self` 与 `ally`(例如同为 `AtkType=1` 时,不能混用同一默认目标)。
|
||||||
|
4. 保持旧协议兼容:旧入口不报错,但按上述兜底规则执行。
|
||||||
|
|
||||||
194
docs/fight-input-controller-binding.md
Normal file
194
docs/fight-input-controller-binding.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# Fight Input 控制绑定说明
|
||||||
|
|
||||||
|
日期:2026-04-04
|
||||||
|
|
||||||
|
## 1. 背景
|
||||||
|
|
||||||
|
当前战斗模型中,一个 `Input` 对应一个战斗站位(`actorIndex`)。
|
||||||
|
每个 `Input` 通过 `Input.Player` 绑定操作者。
|
||||||
|
|
||||||
|
当前建战主路径已收敛为:`WithFightInputs(ourInputs, oppInputs)`。
|
||||||
|
即:先由调用方创建并组装双方 `Input`,再传给战斗模块。
|
||||||
|
|
||||||
|
为了同时支持以下两种玩法,新增了可配置绑定策略:
|
||||||
|
|
||||||
|
1. 双打:一个玩家控制多个站位(单人多 `Input`)
|
||||||
|
2. 组队:一个玩家控制一个站位(每人一个 `Input`)
|
||||||
|
|
||||||
|
## 2. 绑定策略
|
||||||
|
|
||||||
|
文件:`logic/service/fight/new_options.go`
|
||||||
|
|
||||||
|
- `InputControllerBindingKeep`
|
||||||
|
- 含义:保持输入中已有 `Input.Player` 绑定,不覆盖
|
||||||
|
- 适用:调用方已手动构造 `Input` 绑定
|
||||||
|
|
||||||
|
- `InputControllerBindingSingle`
|
||||||
|
- 含义:单侧全部站位统一绑定为 `players[0]`
|
||||||
|
- 适用:双打中一个人控制多个站位
|
||||||
|
|
||||||
|
- `InputControllerBindingPerSlot`
|
||||||
|
- 含义:按站位顺序绑定为 `players[i]`
|
||||||
|
- 适用:组队中一人一个站位
|
||||||
|
- 说明:当 `players` 数量不足时,回退绑定 `players[0]`
|
||||||
|
|
||||||
|
## 3. 选项接口
|
||||||
|
|
||||||
|
文件:`logic/service/fight/new_options.go`
|
||||||
|
|
||||||
|
新增选项:
|
||||||
|
|
||||||
|
```go
|
||||||
|
WithInputControllerBinding(mode int)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 生效时机
|
||||||
|
|
||||||
|
文件:`logic/service/fight/new.go`
|
||||||
|
|
||||||
|
在 `buildFight` 中,构建完 `Our/Opp` 输入后,先执行控制绑定,再执行上下文绑定:
|
||||||
|
|
||||||
|
1. `bindInputControllers(f.Our, f.OurPlayers, opts.controllerBinding)`
|
||||||
|
2. `bindInputControllers(f.Opp, f.OppPlayers, opts.controllerBinding)`
|
||||||
|
3. `bindInputFightContext(...)`
|
||||||
|
4. `linkTeamViews()`
|
||||||
|
5. `linkOppInputs()`
|
||||||
|
|
||||||
|
## 5. 使用示例
|
||||||
|
|
||||||
|
### 5.1 双打(单人控多站位)
|
||||||
|
|
||||||
|
```go
|
||||||
|
fight.NewFightWithOptions(
|
||||||
|
fight.WithFightPlayersOnSide(
|
||||||
|
[]common.PlayerI{ourPlayer},
|
||||||
|
[]common.PlayerI{oppPlayer},
|
||||||
|
),
|
||||||
|
fight.WithFightInputs(ourInputs, oppInputs),
|
||||||
|
fight.WithInputControllerBinding(fight.InputControllerBindingSingle),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 组队(一人一个站位)
|
||||||
|
|
||||||
|
```go
|
||||||
|
fight.NewFightWithOptions(
|
||||||
|
fight.WithFightPlayersOnSide(
|
||||||
|
[]common.PlayerI{ourP1, ourP2},
|
||||||
|
[]common.PlayerI{oppP1, oppP2},
|
||||||
|
),
|
||||||
|
fight.WithFightInputs(ourInputs, oppInputs),
|
||||||
|
fight.WithInputControllerBinding(fight.InputControllerBindingPerSlot),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 仅传已绑定 Input(推荐灵活接入)
|
||||||
|
|
||||||
|
```go
|
||||||
|
ourInputs := []*input.Input{
|
||||||
|
input.NewInput(nil, ourP1), // 站位0
|
||||||
|
input.NewInput(nil, ourP2), // 站位1
|
||||||
|
}
|
||||||
|
oppInputs := []*input.Input{
|
||||||
|
input.NewInput(nil, oppP1), // 站位0
|
||||||
|
input.NewInput(nil, oppP2), // 站位1
|
||||||
|
}
|
||||||
|
|
||||||
|
fc, err := fight.NewFightWithOptions(
|
||||||
|
fight.WithFightInputs(ourInputs, oppInputs),
|
||||||
|
// 不传 WithFightPlayersOnSide 也可
|
||||||
|
// owner/opponent 与 side players 会从 inputs 自动提取
|
||||||
|
)
|
||||||
|
_ = fc
|
||||||
|
_ = err
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:`InputControllerBindingSingle/PerSlot` 会覆盖 `ourInputs/oppInputs` 中原有的 `Input.Player` 绑定;`Keep` 不覆盖。
|
||||||
|
|
||||||
|
## 6. 新模式绑定实例(逐模式)
|
||||||
|
|
||||||
|
以下示例假设我方有两个站位:`ourInputs[0]`、`ourInputs[1]`。
|
||||||
|
|
||||||
|
### 6.1 Keep(保持输入原绑定)
|
||||||
|
|
||||||
|
调用:
|
||||||
|
|
||||||
|
```go
|
||||||
|
fight.NewFightWithOptions(
|
||||||
|
fight.WithFightInputs(ourInputs, oppInputs),
|
||||||
|
fight.WithInputControllerBinding(fight.InputControllerBindingKeep),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
输入(调用前):
|
||||||
|
|
||||||
|
- `ourInputs[0].Player = ourP1`
|
||||||
|
- `ourInputs[1].Player = ourP2`
|
||||||
|
|
||||||
|
结果(调用后):
|
||||||
|
|
||||||
|
- `ourInputs[0].Player = ourP1`
|
||||||
|
- `ourInputs[1].Player = ourP2`
|
||||||
|
|
||||||
|
适用:调用方已提前把每个站位绑定好,不希望框架覆盖。
|
||||||
|
|
||||||
|
### 6.2 Single(单人控制全部站位)
|
||||||
|
|
||||||
|
调用:
|
||||||
|
|
||||||
|
```go
|
||||||
|
fight.NewFightWithOptions(
|
||||||
|
fight.WithFightPlayersOnSide(
|
||||||
|
[]common.PlayerI{ourCaptain},
|
||||||
|
[]common.PlayerI{oppCaptain},
|
||||||
|
),
|
||||||
|
fight.WithFightInputs(ourInputs, oppInputs),
|
||||||
|
fight.WithInputControllerBinding(fight.InputControllerBindingSingle),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
输入(调用前):
|
||||||
|
|
||||||
|
- `ourInputs[0].Player = ourP1`
|
||||||
|
- `ourInputs[1].Player = ourP2`
|
||||||
|
|
||||||
|
结果(调用后):
|
||||||
|
|
||||||
|
- `ourInputs[0].Player = ourCaptain`
|
||||||
|
- `ourInputs[1].Player = ourCaptain`
|
||||||
|
|
||||||
|
适用:双打或多站位由同一玩家操作。
|
||||||
|
|
||||||
|
### 6.3 PerSlot(按站位顺序绑定玩家)
|
||||||
|
|
||||||
|
调用:
|
||||||
|
|
||||||
|
```go
|
||||||
|
fight.NewFightWithOptions(
|
||||||
|
fight.WithFightPlayersOnSide(
|
||||||
|
[]common.PlayerI{ourP1, ourP2},
|
||||||
|
[]common.PlayerI{oppP1, oppP2},
|
||||||
|
),
|
||||||
|
fight.WithFightInputs(ourInputs, oppInputs),
|
||||||
|
fight.WithInputControllerBinding(fight.InputControllerBindingPerSlot),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
输入(调用前):
|
||||||
|
|
||||||
|
- `ourInputs[0].Player = anyA`
|
||||||
|
- `ourInputs[1].Player = anyB`
|
||||||
|
|
||||||
|
结果(调用后):
|
||||||
|
|
||||||
|
- `ourInputs[0].Player = ourP1`
|
||||||
|
- `ourInputs[1].Player = ourP2`
|
||||||
|
|
||||||
|
补位规则:若 `players` 数量不足(例如只传一个 `ourP1`),剩余站位回退绑定 `players[0]`。
|
||||||
|
|
||||||
|
## 7. 注意事项
|
||||||
|
|
||||||
|
1. 默认模式是 `InputControllerBindingKeep`,不影响现有调用。
|
||||||
|
2. 若传入 `WithFightInputs(...)` 且每个 `Input.Player` 已预先绑定,可继续用默认模式。
|
||||||
|
3. 仅传 `WithFightInputs(...)` 也可工作:框架会从 `ourInputs/oppInputs` 自动提取 `ourPlayers/oppPlayers`,并以各侧首位玩家作为 owner/opponent。
|
||||||
|
4. 推荐在新组队逻辑中显式传 `WithInputControllerBinding(...)`,避免调用方歧义。
|
||||||
327
docs/fight-multi-battle-refactor-task-2026-04-04.md
Normal file
327
docs/fight-multi-battle-refactor-task-2026-04-04.md
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
# 战斗多单位模式改造交接文档(2026-04-04)
|
||||||
|
|
||||||
|
## 0. 2026-04-04 本轮完成情况
|
||||||
|
|
||||||
|
本轮已完成以下落地项:
|
||||||
|
|
||||||
|
- 动作提交改为按 `playerID + actorIndex` 去重,同一玩家同回合的多个上场位动作不会再互相覆盖
|
||||||
|
- 主循环已从“双动作入口”改为“动作列表入口”,`resolveRound` 现接收并处理 `[]action.BattleActionI`
|
||||||
|
- 回合结算增加了基于优先级和速度的统一排序,并按跨阵营动作对子顺序执行,保留现有 `enterturn(first, second)` 兼容层
|
||||||
|
- 技能和道具的目标选择已接入 `targetIndex`,不再固定打对面 `0` 号位
|
||||||
|
- 切宠同步改为携带 `actorIndex`,同一玩家多上场位的切宠播报不再冲突
|
||||||
|
- 开战同步结构新增当前战斗位数组,同时保留 `Info1/Info2` 兼容旧结构
|
||||||
|
- `FightI` 已补充 `UseSkillAt/ChangePetAt/UseItemAt/GetCurrPETAt`
|
||||||
|
- `NewFight` 已改为包装 `NewFightWithOptions(...)`,创建阶段开始支持 option/builder 扩展
|
||||||
|
- `Ctx` 已拆分为 `LegacySides + EffectBinding`,effect 本体上挂载上下文,并补充 `Source/Carrier/Target`
|
||||||
|
- 核心执行链已开始迁移到真实 source/target 语义:`AddEffect`、`Exec`、`Damage`、`SetProp`、主技能结算流程会注入实际对手上下文
|
||||||
|
- 已迁移一批公共/高复用 effect 到新语义,包括状态基类、击败触发、物攻附加状态、`1097-1101`、`680-690`、部分魂印基础逻辑
|
||||||
|
- 本轮继续完成了 `1263-1287`、`1288-1312`、`1448-1472`、`1473-1497` 四组 effect 的迁移,已不再直接依赖 `Ctx().Our/Opp`,统一改为 `CarrierInput()/OpponentInput()` 访问当前承载侧与对位侧
|
||||||
|
- 增加了动作队列的基础单测,覆盖“同玩家不同槽位保留”和“同槽位动作替换”
|
||||||
|
|
||||||
|
本轮仍保留的限制:
|
||||||
|
|
||||||
|
- `enterturn` 和大量 `effect/node` 逻辑仍是双动作上下文,因此当前实现采用“动作列表排序 + 跨阵营配对兼容执行”的过渡方案,而不是一次性重写所有效果系统
|
||||||
|
- `NewFight` 仍按现有建房流程创建双方 1 个战斗位;本轮打通的是多战斗位结算骨架和接口,不是外部建房入口的全量切换
|
||||||
|
- 大量具体 effect 仍在使用旧的 `Ctx().Our/Opp` 语义;当前已迁移的是上下文承载方式、执行链和部分公共基类,具体 effect 仍需继续分批迁移
|
||||||
|
|
||||||
|
### 0.1 effect 迁移增量记录
|
||||||
|
|
||||||
|
本轮新增完成:
|
||||||
|
|
||||||
|
- `logic/service/fight/effect/1263_1287.go`
|
||||||
|
- `logic/service/fight/effect/1288_1312.go`
|
||||||
|
- `logic/service/fight/effect/1448_1472.go`
|
||||||
|
- `logic/service/fight/effect/1473_1497.go`
|
||||||
|
|
||||||
|
这两组文件当前迁移策略是:
|
||||||
|
|
||||||
|
- 旧语义中的 `Our` 统一视为“当前执行/承载该 effect 的输入侧”,迁移为 `CarrierInput()`
|
||||||
|
- 旧语义中的 `Opp` 统一迁移为当前结算上下文里的对位输入侧,即 `OpponentInput()`
|
||||||
|
- 暂不在这一轮强行把所有 effect 重写成纯 `Source/Target/Carrier` 三元语义;先保证 hostile sub-effect、回合类 effect、挂在对手身上的限制类 effect 都不再依赖 legacy 字段访问
|
||||||
|
|
||||||
|
### 0.2 下一批待迁移队列
|
||||||
|
|
||||||
|
高密度遗留文件已继续向后推进,`1448-1497` 这两个分段本轮已清理完成。
|
||||||
|
|
||||||
|
下一轮继续迁移时,建议直接对 effect 包执行一次全量扫描,按“仍包含 `Ctx().Our/Opp` 的 grouped file”继续往后收口,而不是再只盯固定编号段。
|
||||||
|
|
||||||
|
## 1. 任务目标
|
||||||
|
|
||||||
|
将当前战斗系统从“每回合双方各 1 个动作”的模型,改造成支持多上场位、多操作者的统一回合模型,最终支持以下 3 种战斗模式:
|
||||||
|
|
||||||
|
1. `1玩家:N精灵:1上场 VS 1玩家:N精灵:1上场`
|
||||||
|
2. `N玩家:N精灵:N上场 VS N玩家:N精灵:N上场`
|
||||||
|
3. `1玩家:N精灵:N上场 VS 1玩家:N精灵:N上场`
|
||||||
|
|
||||||
|
当前代码只完整支持模式 1。模式 2 和模式 3 只做了结构铺垫,还没有真正打通。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 当前已完成的基础改造
|
||||||
|
|
||||||
|
以下结构改造已经落地:
|
||||||
|
|
||||||
|
- `FightC.Our/Opp` 已改成数组,表示战场单位数组,不再是单对象。
|
||||||
|
- `input.Input.CurrentPet` 已改成 `CurPet`,并且是数组。
|
||||||
|
- `FightC.OurPlayers/OppPlayers` 已加入,用于表达操作者数组。
|
||||||
|
- 战斗单位与操作者已解耦:
|
||||||
|
- `Our/Opp` 表示战斗位
|
||||||
|
- `OurPlayers/OppPlayers` 表示操作这些战斗位的玩家
|
||||||
|
- `BattlePetEntity` 已支持绑定控制者:`ControllerUserID`
|
||||||
|
- 动作模型已支持:
|
||||||
|
- `ActorIndex`
|
||||||
|
- `TargetIndex`
|
||||||
|
- 已提供 indexed 入口:
|
||||||
|
- `UseSkillAt(c, skillID, actorIndex, targetIndex)`
|
||||||
|
- `ChangePetAt(c, petID, actorIndex)`
|
||||||
|
- `UseItemAt(c, catchTime, itemID, actorIndex, targetIndex)`
|
||||||
|
|
||||||
|
当前默认行为仍等价于:
|
||||||
|
|
||||||
|
- `actorIndex = 0`
|
||||||
|
- `targetIndex = 0`
|
||||||
|
|
||||||
|
也就是当前模式下仍然是操作和结算 `0` 号单位。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 当前未完成的核心问题
|
||||||
|
|
||||||
|
### 3.1 回合模型仍然是“双动作模型”
|
||||||
|
|
||||||
|
目前主流程仍然是每回合只处理双方两个动作,而不是处理一个动作列表。
|
||||||
|
|
||||||
|
关键位置:
|
||||||
|
|
||||||
|
- `logic/service/fight/loop.go`
|
||||||
|
- `collectPlayerActions(...)` 只收 2 个动作
|
||||||
|
- `resolveRound(p1Action, p2Action)` 只结算 2 个动作
|
||||||
|
|
||||||
|
这意味着:
|
||||||
|
|
||||||
|
- 模式 2 无法支持双方多个操作者或多个上场位同时行动
|
||||||
|
- 模式 3 无法支持同一玩家控制多个上场位分别出手
|
||||||
|
|
||||||
|
### 3.2 动作提交仍按 `playerID` 去重
|
||||||
|
|
||||||
|
当前动作队列逻辑仍以 `playerID` 作为主要识别维度。
|
||||||
|
|
||||||
|
关键位置:
|
||||||
|
|
||||||
|
- `logic/service/fight/action.go`
|
||||||
|
- `submitAction(...)`
|
||||||
|
|
||||||
|
这会导致:
|
||||||
|
|
||||||
|
- 同一玩家在同一回合给多个上场位下达动作时,动作会互相覆盖或无法完整保留
|
||||||
|
|
||||||
|
这一点对模式 3 是直接阻塞,对模式 2 也不够健壮。
|
||||||
|
|
||||||
|
### 3.3 切宠和当前上场位逻辑仍大量默认使用 `CurPet[0]`
|
||||||
|
|
||||||
|
虽然 `CurPet` 已经是数组,但主流程中不少逻辑仍固定操作 `0` 号位。
|
||||||
|
|
||||||
|
典型影响:
|
||||||
|
|
||||||
|
- 死亡换宠
|
||||||
|
- 主动换宠
|
||||||
|
- 当前出手单位检查
|
||||||
|
- 当前目标单位检查
|
||||||
|
|
||||||
|
这部分需要按 `actorIndex` 或上场槽位改造。
|
||||||
|
|
||||||
|
### 3.4 开战协议仍然只有两个当前单位
|
||||||
|
|
||||||
|
当前开战下发协议仍然是双单位结构。
|
||||||
|
|
||||||
|
关键位置:
|
||||||
|
|
||||||
|
- `logic/service/fight/info/info.go`
|
||||||
|
- `FightStartOutboundInfo`
|
||||||
|
- 仍只有 `Info1` 和 `Info2`
|
||||||
|
|
||||||
|
这不适合多上场位模式。
|
||||||
|
|
||||||
|
### 3.5 公共接口仍是旧的单单位接口
|
||||||
|
|
||||||
|
关键位置:
|
||||||
|
|
||||||
|
- `logic/service/common/fight.go`
|
||||||
|
- `FightI`
|
||||||
|
|
||||||
|
目前接口仍只有:
|
||||||
|
|
||||||
|
- `UseSkill(c, id)`
|
||||||
|
- `ChangePet(c, id)`
|
||||||
|
- `UseItem(c, cacthid, itemid)`
|
||||||
|
|
||||||
|
而 indexed 版本只存在于具体实现 `FightC` 上,没有进入正式接口层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 当前实现与目标模式的对应关系
|
||||||
|
|
||||||
|
### 4.1 模式 1
|
||||||
|
|
||||||
|
`1玩家:N精灵:1上场 VS 1玩家:N精灵:1上场`
|
||||||
|
|
||||||
|
当前支持。
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 当前默认就是操作 `0` 号单位
|
||||||
|
- 当前默认就是攻击 `0` 号目标
|
||||||
|
- 当前回合系统仍是每边 1 个动作,这与模式 1 一致
|
||||||
|
|
||||||
|
### 4.2 模式 2
|
||||||
|
|
||||||
|
`N玩家:N精灵:N上场 VS N玩家:N精灵:N上场`
|
||||||
|
|
||||||
|
当前不支持。
|
||||||
|
|
||||||
|
直接原因:
|
||||||
|
|
||||||
|
- 一回合只收 2 个动作
|
||||||
|
- 一回合只结算 2 个动作
|
||||||
|
- 协议仍只同步 2 个当前上场位
|
||||||
|
|
||||||
|
### 4.3 模式 3
|
||||||
|
|
||||||
|
`1玩家:N精灵:N上场 VS 1玩家:N精灵:N上场`
|
||||||
|
|
||||||
|
当前不支持。
|
||||||
|
|
||||||
|
直接原因:
|
||||||
|
|
||||||
|
- 同一玩家的多个动作无法作为同回合动作列表完整保留
|
||||||
|
- 主流程仍不是按动作列表统一排序和执行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 需要完成的工作
|
||||||
|
|
||||||
|
### 5.1 改造动作收集模型
|
||||||
|
|
||||||
|
将当前“每边 1 个动作”的模型改成“每个可操作上场位 1 个动作”的模型。
|
||||||
|
|
||||||
|
至少需要做到:
|
||||||
|
|
||||||
|
- 同一玩家可以在同一回合提交多个动作
|
||||||
|
- 每个动作能区分是哪个上场位发出的
|
||||||
|
- 每个动作能区分目标上场位
|
||||||
|
|
||||||
|
建议将动作唯一键至少扩为:
|
||||||
|
|
||||||
|
- `playerID`
|
||||||
|
- `actorIndex`
|
||||||
|
|
||||||
|
### 5.2 改造回合结算模型
|
||||||
|
|
||||||
|
将当前:
|
||||||
|
|
||||||
|
- `resolveRound(p1Action, p2Action)`
|
||||||
|
|
||||||
|
改成:
|
||||||
|
|
||||||
|
- `resolveRound(actions []action.BattleActionI)`
|
||||||
|
|
||||||
|
并完成:
|
||||||
|
|
||||||
|
- 动作列表排序
|
||||||
|
- 按优先级、速度等规则统一排序
|
||||||
|
- 排序后逐个结算
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
- 当前 effect/node 体系里仍有大量“双动作”接口,不适合一次性全部重写
|
||||||
|
- 建议先在主流程做兼容层,逐步过渡
|
||||||
|
|
||||||
|
### 5.3 按槽位处理切宠与死亡换宠
|
||||||
|
|
||||||
|
将当前固定 `CurPet[0]` 的逻辑改成按槽位处理:
|
||||||
|
|
||||||
|
- 主动换宠
|
||||||
|
- 被动死亡换宠
|
||||||
|
- 死亡校验
|
||||||
|
- 出手资格判断
|
||||||
|
|
||||||
|
### 5.4 增加开战与战斗同步结构
|
||||||
|
|
||||||
|
将当前的双单位同步结构扩成可支持多上场位的结构。但是保持协议结构不变。现在是固定两个,可以改成数组来实现
|
||||||
|
|
||||||
|
重点是:
|
||||||
|
|
||||||
|
- 开战协议
|
||||||
|
- 当前上场位同步
|
||||||
|
- 切宠同步
|
||||||
|
- 可能的回合播报结构
|
||||||
|
|
||||||
|
### 5.5 补齐公共接口
|
||||||
|
|
||||||
|
将 indexed 版本能力补进接口层,避免只能通过具体实现类型访问。
|
||||||
|
|
||||||
|
建议新增类似接口:
|
||||||
|
|
||||||
|
- `UseSkillAt(...)`
|
||||||
|
- `ChangePetAt(...)`
|
||||||
|
- `UseItemAt(...)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 推荐实施顺序
|
||||||
|
|
||||||
|
建议按下面顺序推进,避免一次性改动面过大:
|
||||||
|
|
||||||
|
1. 先改动作队列和动作收集逻辑
|
||||||
|
2. 再改回合结算为动作列表
|
||||||
|
3. 再改切宠和死亡换宠按槽位处理
|
||||||
|
4. 最后改协议和正式接口
|
||||||
|
|
||||||
|
不建议一开始就全量重写 effect/node 接口,因为当前大量效果实现仍假设双动作上下文。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 建议重点查看文件
|
||||||
|
|
||||||
|
- `logic/service/fight/action.go`
|
||||||
|
- `logic/service/fight/loop.go`
|
||||||
|
- `logic/service/fight/fightc.go`
|
||||||
|
- `logic/service/fight/input.go`
|
||||||
|
- `logic/service/fight/input/input.go`
|
||||||
|
- `logic/service/fight/action/BattleAction.go`
|
||||||
|
- `logic/service/fight/info/info.go`
|
||||||
|
- `logic/service/common/fight.go`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 完成标准
|
||||||
|
|
||||||
|
至少满足以下条件,才算这次改造完成:
|
||||||
|
|
||||||
|
1. 同一玩家可以在同一回合给多个上场位分别提交动作,动作不会互相覆盖
|
||||||
|
2. 双方多个上场位可以在同一回合统一排序并依次结算
|
||||||
|
3. 攻击目标位可选,不再默认只能打对面 `0` 号
|
||||||
|
4. 切宠可以按上场槽位处理
|
||||||
|
5. 模式 1 不回归
|
||||||
|
6. 代码编译通过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 最低验证要求
|
||||||
|
|
||||||
|
至少执行:
|
||||||
|
|
||||||
|
- `cd /workspace/logic && go build ./...`
|
||||||
|
- `cd /workspace/logic && go test ./service/fight/effect`
|
||||||
|
|
||||||
|
如果本轮改动较大,建议再补一轮:
|
||||||
|
|
||||||
|
- `cd /workspace/logic && go test ./...`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 额外提醒
|
||||||
|
|
||||||
|
- 当前仓库工作区可能是脏的,不要回滚无关修改。
|
||||||
|
- 这次改造的真正核心不是结构字段改数组,而是把回合系统从“双动作模型”改成“动作列表模型”。
|
||||||
|
- 已有 `ActorIndex/TargetIndex` 只是入口铺垫,不代表多单位模式已经完成。
|
||||||
224
docs/pvp-login-rpc-match-design-2026-04-09.md
Normal file
224
docs/pvp-login-rpc-match-design-2026-04-09.md
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
# PVP Match Via RPC, Battle Via Redis
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
本次调整先不解决 `login` 更新期间的排队保活和补偿问题,只收敛到一个更简单、可控的方案:
|
||||||
|
|
||||||
|
- 匹配请求走 `logic -> login` 的同步 RPC
|
||||||
|
- 对战过程仍走 `logic` 本地战斗 + Redis 转发战斗指令
|
||||||
|
- `login` 不可用时,`logic` 直接返回“匹配服务不可用”
|
||||||
|
- 前端通过轮询重新发起 / 更新匹配请求,不在后端保留离线补偿队列
|
||||||
|
|
||||||
|
这个方案的核心是:先把“能否立即判断匹配服务可用”做好,不继续依赖 Redis PubSub 做匹配入口。
|
||||||
|
|
||||||
|
## 当前现状
|
||||||
|
|
||||||
|
### 现有匹配入口
|
||||||
|
|
||||||
|
- 前端 `2458` 进入 [logic/controller/fight_巅峰.go](/workspace/logic/controller/fight_巅峰.go#L19)
|
||||||
|
- 当前 `JoINtop` 直接调用 [logic/service/fight/pvp/service.go](/workspace/logic/service/fight/pvp/service.go#L83) 的 `JoinPeakQueue`
|
||||||
|
- `JoinPeakQueue` 当前实现是本地建 `localQueueTicket`,并通过 Redis `publish` 发 `queue_join`
|
||||||
|
|
||||||
|
### 现有跨服协调
|
||||||
|
|
||||||
|
- `logic` 侧订阅 PVP Redis topic 的入口在 [common/rpc/func.go](/workspace/common/rpc/func.go#L153)
|
||||||
|
- PVP 匹配状态当前存在 `logic/service/fight/pvp/service.go` 的 manager 内存里:
|
||||||
|
- `queues`
|
||||||
|
- `lastSeen`
|
||||||
|
- `localQueues`
|
||||||
|
- `sessions`
|
||||||
|
- `userSession`
|
||||||
|
|
||||||
|
### 现有 RPC 能力
|
||||||
|
|
||||||
|
- `logic` 启动时通过 [common/rpc/rpc.go](/workspace/common/rpc/rpc.go#L113) 建立到 `login` 的 RPC client
|
||||||
|
- `login` 的 `/rpc/*` 入口绑定在 [modules/base/middleware/middleware.go](/workspace/modules/base/middleware/middleware.go#L152)
|
||||||
|
- `login` 侧 RPC server 由 [common/rpc/rpc.go](/workspace/common/rpc/rpc.go#L101) 暴露
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
|
||||||
|
Redis PubSub 适合“广播消息”,不适合“同步判断服务是否可用”。
|
||||||
|
|
||||||
|
如果继续让匹配入口走 PubSub:
|
||||||
|
|
||||||
|
- `logic` 无法在请求当下知道 `login` 是否真能处理
|
||||||
|
- `login` 更新、重启、未订阅时,匹配请求可能直接丢失
|
||||||
|
- 前端即使轮询,也只是重复投递,不能精确表达“当前匹配服务可用/不可用”
|
||||||
|
|
||||||
|
## 收敛后的职责划分
|
||||||
|
|
||||||
|
### login
|
||||||
|
|
||||||
|
`login` 只负责匹配控制面:
|
||||||
|
|
||||||
|
- 接收 `logic` 发来的同步匹配 RPC
|
||||||
|
- 判断当前匹配服务是否可用
|
||||||
|
- 维护匹配队列
|
||||||
|
- 找到对手后,记录 match 结果
|
||||||
|
- 再通过 Redis 或其他异步方式通知对应 `logic` 开始 Ban/Pick / Battle
|
||||||
|
|
||||||
|
### logic
|
||||||
|
|
||||||
|
`logic` 只负责:
|
||||||
|
|
||||||
|
- 接收前端匹配请求
|
||||||
|
- 同步 RPC 到 `login`
|
||||||
|
- RPC 失败时立即返回“匹配服务不可用”
|
||||||
|
- RPC 成功时返回“排队中”
|
||||||
|
- 收到 match 结果后负责真正 `fight.NewFight(...)`
|
||||||
|
- 对战期间继续使用现有 Redis topic 转发战斗指令
|
||||||
|
|
||||||
|
### Redis
|
||||||
|
|
||||||
|
Redis 只保留在“对战消息面”:
|
||||||
|
|
||||||
|
- `match_found`
|
||||||
|
- `ban_pick_submit`
|
||||||
|
- `battle_command`
|
||||||
|
- `packet_relay`
|
||||||
|
- `session_close`
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- 匹配入口走 RPC
|
||||||
|
- 对战过程走 Redis
|
||||||
|
|
||||||
|
## 推荐目标链路
|
||||||
|
|
||||||
|
### 1. 前端加入/更新匹配
|
||||||
|
|
||||||
|
前端定期轮询 `logic` 的加入/更新接口。
|
||||||
|
|
||||||
|
`logic` 处理流程:
|
||||||
|
|
||||||
|
1. 校验玩家当前战斗状态
|
||||||
|
2. 同步调用 `login` 的匹配 RPC
|
||||||
|
3. 如果 RPC 成功:返回排队中
|
||||||
|
4. 如果 RPC 失败:清理本地匹配状态,返回匹配服务不可用
|
||||||
|
|
||||||
|
### 2. login 完成匹配
|
||||||
|
|
||||||
|
`login` 维护排队队列和匹配结果,匹配成功后:
|
||||||
|
|
||||||
|
1. 确定 host / guest 所在 `logic`
|
||||||
|
2. 通过 Redis 通知两个 `logic`
|
||||||
|
3. host `logic` 开战
|
||||||
|
4. guest `logic` 设置远端代理并进入 Ban/Pick 或战斗态
|
||||||
|
|
||||||
|
### 3. 对战期间
|
||||||
|
|
||||||
|
继续复用当前 `logic/service/fight/pvp/service.go` 内的 Redis 指令转发模式:
|
||||||
|
|
||||||
|
- 战斗操作通过 Redis topic 转发
|
||||||
|
- host `logic` 维持真实战斗对象
|
||||||
|
- guest `logic` 维持 remote proxy
|
||||||
|
|
||||||
|
## 失败语义
|
||||||
|
|
||||||
|
本阶段不做补偿,不做离线保队列。
|
||||||
|
|
||||||
|
### login 不在线
|
||||||
|
|
||||||
|
如果 `logic -> login` RPC 调用失败:
|
||||||
|
|
||||||
|
- 本次匹配直接失败
|
||||||
|
- `logic` 清理本地匹配状态
|
||||||
|
- 返回前端“匹配服务不可用”
|
||||||
|
|
||||||
|
### 前端轮询停止
|
||||||
|
|
||||||
|
如果前端不再轮询:
|
||||||
|
|
||||||
|
- 视为用户不再持续请求匹配
|
||||||
|
- `logic` 不负责继续保活
|
||||||
|
- 是否从 `login` 队列移除,由 `login` 的超时策略决定
|
||||||
|
|
||||||
|
### login 更新中
|
||||||
|
|
||||||
|
如果 `login` 正在更新:
|
||||||
|
|
||||||
|
- `logic` 的同步 RPC 会失败
|
||||||
|
- 前端当前轮询会收到“匹配服务不可用”
|
||||||
|
- 等 `login` 恢复后,前端下一轮再发起匹配
|
||||||
|
|
||||||
|
这是本阶段明确接受的行为,不在后端做补偿。
|
||||||
|
|
||||||
|
## 最小实现建议
|
||||||
|
|
||||||
|
### 一、先增加 RPC 健康/匹配接口
|
||||||
|
|
||||||
|
在 [common/rpc/rpc.go](/workspace/common/rpc/rpc.go) 增加面向 `logic -> login` 的 RPC 方法。
|
||||||
|
|
||||||
|
建议最小接口:
|
||||||
|
|
||||||
|
- `MatchJoinOrUpdate(PVPMatchJoinPayload) error`
|
||||||
|
- `MatchCancel(userID) error`
|
||||||
|
|
||||||
|
如果需要单独健康检查,也可以加:
|
||||||
|
|
||||||
|
- `MatchPing() error`
|
||||||
|
|
||||||
|
但在最小方案里,`MatchJoinOrUpdate` 自身就可以承担健康检查职责。
|
||||||
|
|
||||||
|
### 二、logic 的匹配入口改为同步 RPC
|
||||||
|
|
||||||
|
改造 [logic/controller/fight_巅峰.go](/workspace/logic/controller/fight_巅峰.go#L19) 和 [logic/service/fight/pvp/service.go](/workspace/logic/service/fight/pvp/service.go#L83):
|
||||||
|
|
||||||
|
- 入口不再直接发布 `queue_join`
|
||||||
|
- 先发 RPC 到 `login`
|
||||||
|
- 成功才更新本地匹配状态
|
||||||
|
- 失败直接返回错误
|
||||||
|
- 取消匹配时通过 `MatchCancel` 做 best-effort 清理
|
||||||
|
|
||||||
|
### 三、保留 Redis 对战链路
|
||||||
|
|
||||||
|
[logic/service/fight/pvp/service.go](/workspace/logic/service/fight/pvp/service.go#L170) 之后的 Redis 消费、match result 处理、Ban/Pick、战斗 relay 不需要一次性重写,可以继续保留。
|
||||||
|
|
||||||
|
调整重点是:
|
||||||
|
|
||||||
|
- 不再让匹配入口依赖 PubSub
|
||||||
|
- 让对战过程继续走 Redis
|
||||||
|
|
||||||
|
## 对前端的要求
|
||||||
|
|
||||||
|
前端不要无脑重复“新 join”,而是按“轮询更新匹配状态”处理。
|
||||||
|
|
||||||
|
建议行为:
|
||||||
|
|
||||||
|
1. 首次点击匹配时发一次加入
|
||||||
|
2. 匹配中每隔 `3~5s` 轮询一次更新
|
||||||
|
3. 如果返回“匹配服务不可用”,前端退出匹配态并提示
|
||||||
|
4. 如果返回“已匹配/进入 Ban/Pick”,前端切换到对应界面
|
||||||
|
|
||||||
|
## 本阶段不做的事
|
||||||
|
|
||||||
|
以下内容明确不在这次最小改造内:
|
||||||
|
|
||||||
|
- `login` 更新期间的排队保活
|
||||||
|
- 持久化消息补偿
|
||||||
|
- `login` 重启后的队列恢复
|
||||||
|
- Redis Stream 化
|
||||||
|
- 多 `login` 实例协调
|
||||||
|
- 匹配服务自动拉起目标 `logic`
|
||||||
|
|
||||||
|
## 后续可选增强
|
||||||
|
|
||||||
|
如果后面要继续提高可用性,可以再逐步演进为:
|
||||||
|
|
||||||
|
1. 匹配入口仍走 RPC
|
||||||
|
2. `login` 内部把队列落 Redis
|
||||||
|
3. 加入 ticket 和续租机制
|
||||||
|
4. login 更新时支持恢复匹配状态
|
||||||
|
|
||||||
|
但这不是当前阶段的目标。
|
||||||
|
|
||||||
|
## 最终收敛结论
|
||||||
|
|
||||||
|
当前阶段建议明确成一句话:
|
||||||
|
|
||||||
|
`匹配走 RPC,对战走 Redis。`
|
||||||
|
|
||||||
|
对应业务语义:
|
||||||
|
|
||||||
|
- 需要立即判断服务可用性的时候,用 RPC
|
||||||
|
- 需要跨服转发战斗消息的时候,用 Redis
|
||||||
3
go.work
3
go.work
@@ -1,4 +1,4 @@
|
|||||||
go 1.25.0
|
go 1.25
|
||||||
|
|
||||||
use (
|
use (
|
||||||
./common
|
./common
|
||||||
@@ -20,6 +20,7 @@ use (
|
|||||||
./common/utils/sturc
|
./common/utils/sturc
|
||||||
./common/utils/timer
|
./common/utils/timer
|
||||||
./common/utils/xml
|
./common/utils/xml
|
||||||
|
./common/utils/zset
|
||||||
./logic
|
./logic
|
||||||
./login
|
./login
|
||||||
./modules
|
./modules
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
# 屎山代码分析报告
|
|
||||||
|
|
||||||
## 总体评估
|
|
||||||
|
|
||||||
- **质量评分**: 31.03/100
|
|
||||||
- **质量等级**: 🌸 偶有异味 - 基本没事,但是有伤风化
|
|
||||||
- **分析文件数**: 203
|
|
||||||
- **代码总行数**: 20972
|
|
||||||
|
|
||||||
## 质量指标
|
|
||||||
|
|
||||||
| 指标 | 得分 | 权重 | 状态 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| 状态管理 | 4.84 | 0.15 | ✓✓ |
|
|
||||||
| 循环复杂度 | 6.28 | 0.25 | ✓✓ |
|
|
||||||
| 命名规范 | 25.00 | 0.10 | ✓ |
|
|
||||||
| 错误处理 | 35.00 | 0.15 | ○ |
|
|
||||||
| 代码结构 | 45.00 | 0.20 | ○ |
|
|
||||||
| 代码重复度 | 55.00 | 0.15 | • |
|
|
||||||
| 注释覆盖率 | 55.94 | 0.15 | • |
|
|
||||||
|
|
||||||
## 问题文件 (Top 5)
|
|
||||||
|
|
||||||
### 1. /workspace/blazing/common/utils/sturc/field.go (得分: 53.85)
|
|
||||||
**问题分类**: 🔄 复杂度问题:10, 📝 注释问题:1, ⚠️ 其他问题:5
|
|
||||||
|
|
||||||
**主要问题**:
|
|
||||||
- 函数 Size 的循环复杂度较高 (12),建议简化
|
|
||||||
- 函数 packVal 的循环复杂度过高 (23),考虑重构
|
|
||||||
- 函数 Pack 的循环复杂度较高 (14),建议简化
|
|
||||||
- 函数 unpackVal 的循环复杂度过高 (21),考虑重构
|
|
||||||
- 函数 Unpack 的循环复杂度较高 (12),建议简化
|
|
||||||
- 函数 'Size' () 较长 (33 行),可考虑重构
|
|
||||||
- 函数 'Size' () 复杂度过高 (12),建议简化
|
|
||||||
- 函数 'packVal' () 过长 (69 行),建议拆分
|
|
||||||
- 函数 'packVal' () 复杂度严重过高 (23),必须简化
|
|
||||||
- 函数 'Pack' () 较长 (48 行),可考虑重构
|
|
||||||
- 函数 'Pack' () 复杂度过高 (14),建议简化
|
|
||||||
- 函数 'unpackVal' () 过长 (57 行),建议拆分
|
|
||||||
- 函数 'unpackVal' () 复杂度严重过高 (21),必须简化
|
|
||||||
- 函数 'Unpack' () 较长 (33 行),可考虑重构
|
|
||||||
- 函数 'Unpack' () 复杂度过高 (12),建议简化
|
|
||||||
- 代码注释率极低 (1.38%),几乎没有注释
|
|
||||||
|
|
||||||
### 2. /workspace/blazing/common/utils/sturc/fields.go (得分: 46.83)
|
|
||||||
**问题分类**: 🔄 复杂度问题:4, 📝 注释问题:1, ⚠️ 其他问题:2
|
|
||||||
|
|
||||||
**主要问题**:
|
|
||||||
- 函数 Pack 的循环复杂度较高 (12),建议简化
|
|
||||||
- 函数 Unpack 的循环复杂度过高 (21),考虑重构
|
|
||||||
- 函数 'Pack' () 较长 (42 行),可考虑重构
|
|
||||||
- 函数 'Pack' () 复杂度过高 (12),建议简化
|
|
||||||
- 函数 'Unpack' () 过长 (73 行),建议拆分
|
|
||||||
- 函数 'Unpack' () 复杂度严重过高 (21),必须简化
|
|
||||||
- 代码注释率极低 (3.91%),几乎没有注释
|
|
||||||
|
|
||||||
### 3. /workspace/blazing/common/utils/sturc/parse.go (得分: 46.68)
|
|
||||||
**问题分类**: 🔄 复杂度问题:4, 📝 注释问题:1, ⚠️ 其他问题:3
|
|
||||||
|
|
||||||
**主要问题**:
|
|
||||||
- 代码注释率较低 (6.93%),建议增加注释
|
|
||||||
- 函数 parseField 的循环复杂度较高 (13),建议简化
|
|
||||||
- 函数 parseFieldsLocked 的循环复杂度过高 (18),考虑重构
|
|
||||||
- 函数 'parseField' () 过长 (64 行),建议拆分
|
|
||||||
- 函数 'parseField' () 复杂度过高 (13),建议简化
|
|
||||||
- 函数 'parseFieldsLocked' () 过长 (64 行),建议拆分
|
|
||||||
- 函数 'parseFieldsLocked' () 复杂度严重过高 (18),必须简化
|
|
||||||
- 函数 'parseFields' () 较长 (31 行),可考虑重构
|
|
||||||
|
|
||||||
### 4. /workspace/blazing/common/utils/xml/typeinfo.go (得分: 46.13)
|
|
||||||
**问题分类**: 🔄 复杂度问题:6, ⚠️ 其他问题:3
|
|
||||||
|
|
||||||
**主要问题**:
|
|
||||||
- 函数 getTypeInfo 的循环复杂度过高 (18),考虑重构
|
|
||||||
- 函数 structFieldInfo 的循环复杂度过高 (33),考虑重构
|
|
||||||
- 函数 addFieldInfo 的循环复杂度过高 (20),考虑重构
|
|
||||||
- 函数 'getTypeInfo' () 过长 (58 行),建议拆分
|
|
||||||
- 函数 'getTypeInfo' () 复杂度严重过高 (18),必须简化
|
|
||||||
- 函数 'structFieldInfo' () 极度过长 (114 行),必须拆分
|
|
||||||
- 函数 'structFieldInfo' () 复杂度严重过高 (33),必须简化
|
|
||||||
- 函数 'addFieldInfo' () 过长 (66 行),建议拆分
|
|
||||||
- 函数 'addFieldInfo' () 复杂度严重过高 (20),必须简化
|
|
||||||
|
|
||||||
### 5. /workspace/blazing/common/utils/go-jsonrpc/auth/handler.go (得分: 45.61)
|
|
||||||
**问题分类**: 📝 注释问题:1, ⚠️ 其他问题:1
|
|
||||||
|
|
||||||
**主要问题**:
|
|
||||||
- 函数 'ServeHTTP' () 较长 (31 行),可考虑重构
|
|
||||||
- 代码注释率极低 (0.00%),几乎没有注释
|
|
||||||
|
|
||||||
## 改进建议
|
|
||||||
|
|
||||||
### 高优先级
|
|
||||||
- 继续保持当前的代码质量标准
|
|
||||||
|
|
||||||
### 中优先级
|
|
||||||
- 可以考虑进一步优化性能和可读性
|
|
||||||
- 完善文档和注释,便于团队协作
|
|
||||||
|
|
||||||
58
help/三主宠查询.sql
Normal file
58
help/三主宠查询.sql
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
-- 删除:每个多余精灵组中除了最早创建的其余记录
|
||||||
|
WITH pet_group_mapping AS (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
player_id,
|
||||||
|
-- 核心修正:PARTITION BY中直接写分组逻辑
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY
|
||||||
|
player_id,
|
||||||
|
CASE -- 3个一组的分组逻辑
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 1 AND 3 THEN 'group_1_3'
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 4 AND 6 THEN 'group_4_6'
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 7 AND 9 THEN 'group_7_9'
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 301 AND 303 THEN 'group_301_303'
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 304 AND 306 THEN 'group_304_306'
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 307 AND 309 THEN 'group_307_309'
|
||||||
|
END
|
||||||
|
ORDER BY "createTime" ASC
|
||||||
|
) AS rn,
|
||||||
|
-- 定义pet_group用于筛选多余组
|
||||||
|
CASE
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 1 AND 3 THEN 'group_1_3'
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 4 AND 6 THEN 'group_4_6'
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 7 AND 9 THEN 'group_7_9'
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 301 AND 303 THEN 'group_301_303'
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 304 AND 306 THEN 'group_304_306'
|
||||||
|
WHEN (data->>'ID')::INT BETWEEN 307 AND 309 THEN 'group_307_309'
|
||||||
|
END AS pet_group
|
||||||
|
FROM "player_pet"
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
),
|
||||||
|
excess_groups AS (
|
||||||
|
SELECT player_id, pet_group
|
||||||
|
FROM pet_group_mapping
|
||||||
|
WHERE pet_group IS NOT NULL
|
||||||
|
GROUP BY player_id, pet_group
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
)
|
||||||
|
DELETE FROM "player_pet"
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT pgm.id
|
||||||
|
FROM pet_group_mapping pgm
|
||||||
|
INNER JOIN excess_groups eg ON pgm.player_id = eg.player_id AND pgm.pet_group = eg.pet_group
|
||||||
|
WHERE pgm.rn > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
//删除多余的异常融合精灵
|
||||||
|
DELETE FROM "player_pet" pp
|
||||||
|
WHERE
|
||||||
|
pp.deleted_at IS NULL
|
||||||
|
AND pp.is_vip = 0
|
||||||
|
AND (pp.data->>'OldCatchTime')::BIGINT != 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM config_fusion_pet cfp
|
||||||
|
WHERE (pp.data->>'ID')::INT BETWEEN cfp.result_pet_id AND cfp.result_pet_id + 2
|
||||||
|
);
|
||||||
@@ -1 +1,49 @@
|
|||||||
select setval('base_sys_user_id_seq', 10000000, false);
|
select setval('base_sys_user_id_seq', 10000000, false);
|
||||||
|
|
||||||
|
|
||||||
|
-- 清理旧函数
|
||||||
|
ALTER TABLE base_sys_user
|
||||||
|
ALTER COLUMN id SET DEFAULT nextval('base_sys_user_id_seq');
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS shuffle_8digit() CASCADE;
|
||||||
|
|
||||||
|
-- 8位自增 → 纯数字位置互换 → 依旧8位 → 100%不重复
|
||||||
|
CREATE OR REPLACE FUNCTION shuffle_8digit()
|
||||||
|
RETURNS bigint
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
VOLATILE
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
seq bigint;
|
||||||
|
d0 int; d1 int; d2 int; d3 int;
|
||||||
|
d4 int; d5 int; d6 int; d7 int;
|
||||||
|
BEGIN
|
||||||
|
seq := nextval('base_sys_user_id_seq'); -- 原本就是8位
|
||||||
|
|
||||||
|
-- 把 8 位数字拆出来:d7 d6 d5 d4 d3 d2 d1 d0
|
||||||
|
d7 := (seq / 10000000) % 10;
|
||||||
|
d6 := (seq / 1000000) % 10;
|
||||||
|
d5 := (seq / 100000) % 10;
|
||||||
|
d4 := (seq / 10000) % 10;
|
||||||
|
d3 := (seq / 1000) % 10;
|
||||||
|
d2 := (seq / 100) % 10;
|
||||||
|
d1 := (seq / 10) % 10;
|
||||||
|
d0 := seq % 10;
|
||||||
|
|
||||||
|
-- 固定位置互换(一对一置换,绝对不重复)
|
||||||
|
RETURN
|
||||||
|
d3*10000000 +
|
||||||
|
d7*1000000 +
|
||||||
|
d1*100000 +
|
||||||
|
d5*10000 +
|
||||||
|
d2*1000 +
|
||||||
|
d6*100 +
|
||||||
|
d0*10 +
|
||||||
|
d4;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- 启用
|
||||||
|
ALTER TABLE base_sys_user
|
||||||
|
ALTER COLUMN id SET DEFAULT shuffle_8digit();
|
||||||
|
|
||||||
73
help/初始化SPT配置.sql
Normal file
73
help/初始化SPT配置.sql
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
-- 初始化/修复 SPT 配置表(PostgreSQL)
|
||||||
|
-- 用法:在 sun 数据库执行本文件
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS config_spt (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
"createTime" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
"updateTime" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ NULL,
|
||||||
|
is_enable INTEGER NOT NULL DEFAULT 1,
|
||||||
|
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
task_id INTEGER NOT NULL,
|
||||||
|
title VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
pet_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
online INTEGER NOT NULL DEFAULT 1,
|
||||||
|
level INTEGER NOT NULL DEFAULT 1,
|
||||||
|
seat_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
enter_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
description TEXT NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_config_spt_task_id ON config_spt(task_id);
|
||||||
|
|
||||||
|
INSERT INTO config_spt
|
||||||
|
(task_id, title, pet_id, online, level, seat_id, enter_id, description, is_enable, remark)
|
||||||
|
VALUES
|
||||||
|
(301,'蘑菇怪',47,1,1,12,12,'生活在克洛斯星,被艾里逊的液氮冻伤而发狂,使用火焰喷射器可以使它安静下来。制服它可以获得草系精灵小蘑菇。',1,'破除防护罩'),
|
||||||
|
(302,'钢牙鲨',34,1,1,22,21,'海洋星海底的危险怪兽,据说它躲藏的洞穴中有制作黑武士装的黑晶矿石。记住,到海底一定要穿上耐压的潜水套装。',1,''),
|
||||||
|
(303,'里奥斯',42,1,2,17,17,'海盗艾里逊在火山被它困住,战胜它有机会获得火系精灵“胡里亚”。在火山,你会用到喷水装的。',1,'扑灭火焰屏障'),
|
||||||
|
(304,'阿克希亚',50,1,4,40,40,'塞西利亚星的守护者,正义的精灵圣兽,它是不可战胜的,千年来一直等待着宿命的对手。',1,''),
|
||||||
|
(305,'提亚斯',69,1,3,27,27,'云霄星出现了一只极具攻击性的变异精灵,拥有很多蛋的它虽然想要努力呵护自己的孩子却力不从心,看来需要大家帮帮忙啊。',1,''),
|
||||||
|
(306,'雷伊',70,1,3,32,32,'赫尔卡星天空中划过一道闪电,映出了一个酷似精灵的黑影,它全身被电流包围,从它的神态中可以看出它正等待来自各方的挑战。',1,'雷雨天'),
|
||||||
|
(307,'纳多雷',88,1,3,106,106,'在双子阿尔法星上,特派队遇见了一只巨大的精灵,经过多次挑战后,它仍然丝毫无损,赛尔们是否有办法战胜这只精灵呢?',1,''),
|
||||||
|
(308,'雷纳多',113,1,3,49,49,'彪悍的雷纳多盘踞在双子贝塔星上,和双子阿尔法星的纳多雷遥相对应,守护着星球上所有精灵的。',1,''),
|
||||||
|
(309,'尤纳斯',132,1,4,314,314,'黑暗之门的制造者,拥有能够抵御一切的暗影屏障和所有能量来源的黑暗之核。',1,''),
|
||||||
|
(310,'魔狮迪露',187,1,4,53,53,'魔狮迪露具有神秘的力量,能使自己的体力突破界限,但同时也会受到未知的惩罚。',1,''),
|
||||||
|
(311,'哈莫雷特',216,1,5,60,60,'拥有无比巨大的身躯,集水火草三种原能为一身,龙系的神秘力量使它所向无敌。失忆的它,似乎还有很多谜团……',1,''),
|
||||||
|
(312,'奈尼芬多',264,1,4,325,325,'奈尼芬多是爱迪星的守护者,凄美的歌声连月亮都为之倾倒,据说只有音乐的力量才能够唤醒它。',1,''),
|
||||||
|
(316,'厄尔塞拉',421,1,5,61,61,'浑身散发着各色光芒,任何邪恶在她的光芒下消散无形',1,''),
|
||||||
|
(50,'卡特斯',169,1,2,110,110,'作为暗黑武斗场的试炼精灵,守护着试炼之门,它的气度和风度非同一般,杀气重重很难对付!',1,''),
|
||||||
|
(51,'魔牙鲨',171,1,3,503,503,'暗黑第一门的魔牙鲨,被赋予了传说中的暗黑斗气,隐藏在暗影中攻击时它的能力可以被放大增强。',1,''),
|
||||||
|
(53,'贝鲁基德',174,1,3,504,504,'暗黑第二门的贝鲁基德,暗黑火焰环绕周身,凶悍的外表下藏着善战勇敢的心。',1,''),
|
||||||
|
(55,'巴弗洛',177,1,3,505,505,'勇猛凶横的巴弗洛把守着暗黑武斗场Ⅲ-Ⅰ门,霹雳闪电般的羽翼攻击、震荡心胸的音乐攻击,让人防不胜防。',1,''),
|
||||||
|
(56,'奇拉塔顿',183,1,3,505,505,'勇敢的奇拉塔顿驻守在暗黑武斗场Ⅲ-Ⅱ门的那一边,身为大地之子的它驾驭着反物质能量,纵横无敌。',1,''),
|
||||||
|
(59,'西萨拉斯',195,1,3,506,506,'拥有强大反物质电力的暗黑Ⅳ-Ⅰ门守护者,雷霆之刃震撼寰宇天地,电流之剑穿透空间阻隔,威慑四方。',1,''),
|
||||||
|
(60,'克林卡修',192,1,4,506,506,'暗黑Ⅳ-Ⅱ门守护者冰雪灵兽克林卡修,冰雪之爪具有猛烈的攻击力,果敢不张扬的个性,让它成为忍者般的精灵。',1,''),
|
||||||
|
(76,'卡库',222,1,4,507,507,'暗黑Ⅴ-Ⅰ门是武学之门,守门精灵不张扬、不蛮横、步步为营,每出一招都会致命。',1,''),
|
||||||
|
(77,'赫德卡',224,1,4,507,507,'驻守暗黑V-II门的铁血赫德卡,有着铜墙铁壁的防守能力,有着超级强力的电光炮,它的防御之门你能够开启吗?',1,''),
|
||||||
|
(78,'伊兰罗尼',227,1,5,507,507,'守护暗黑Ⅴ-Ⅲ门的伊兰罗尼是优雅得体的淑女,擅长在裙摆飘飘、光芒闪耀间使出杀手。',1,''),
|
||||||
|
(117,'斯加尔卡',356,1,5,508,508,'暗黑VI-I门的守护者,擅长使用暗黑电能的家伙,比起折磨对手的身体,斯加尔卡更喜欢震慑对手的心灵。',1,''),
|
||||||
|
(118,'艾尔伊洛',297,1,5,508,508,'暗黑VI-II门的守护者,历经了炼狱洗礼的艾尔伊洛开始崇尚爽快的攻击方式,喜欢凭借精湛的技巧近距地伤害对手。',1,''),
|
||||||
|
(119,'布林克克',359,1,5,508,508,'暗黑VI-III门的守护者,拥有海妖之力的庇护,体内充满着混沌的能量企图吞噬整个海洋。',1,''),
|
||||||
|
(502,'魔花使者',438,1,5,509,509,'暗黑VII-I门的守护者,比恩特的进化形态,浑身散发着反物质世界中的黑暗气息,散发出来的毒粉是它的致命武器。',1,''),
|
||||||
|
(503,'莫尔加斯',441,1,5,509,509,'暗黑VII-II门的守护者,莫鲁格尔的进化形态,受到了反物质世界的影响,浑身被黑暗所包围,拥有极强的防御能力,所有攻击在它面前都显得非常渺小。',1,''),
|
||||||
|
(504,'萨诺拉斯',435,1,5,509,509,'暗黑VII-III门的守护者,萨诺的进化形态,经过岩浆洗礼的皮肤拥有独特的降温功能,即使在极其炎热的环境下依然不受影响。',1,''),
|
||||||
|
(606,'帕多尼',656,1,5,510,510,'暗黑Ⅷ-Ⅰ门的守护者,浑身充斥着暗黑能量,暗黑能量会随着它的歌神散发出来。',1,''),
|
||||||
|
(607,'加洛德',659,1,5,510,510,'暗黑Ⅷ-Ⅱ门的守护,暗黑能量的注入,使它的脾气变得暴躁,擅长与对手近身搏斗,浑身的尖刺、催生出的植物都是它进攻的利器。',1,''),
|
||||||
|
(608,'萨多拉尼',661,1,5,510,510,'暗黑Ⅷ-Ⅲ门的守护,将暗黑能量融入自身,肢体变得非常结实有力,虽然体积很小,但是却拥有了堪比巨龙的神力。',1,'')
|
||||||
|
ON CONFLICT (task_id) DO UPDATE
|
||||||
|
SET
|
||||||
|
title = EXCLUDED.title,
|
||||||
|
pet_id = EXCLUDED.pet_id,
|
||||||
|
online = EXCLUDED.online,
|
||||||
|
level = EXCLUDED.level,
|
||||||
|
seat_id = EXCLUDED.seat_id,
|
||||||
|
enter_id = EXCLUDED.enter_id,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
is_enable = EXCLUDED.is_enable,
|
||||||
|
remark = EXCLUDED.remark,
|
||||||
|
"updateTime" = NOW();
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
File diff suppressed because it is too large
Load Diff
16
help/查询多余物品.sql
Normal file
16
help/查询多余物品.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
SELECT
|
||||||
|
t1.*
|
||||||
|
FROM player_item t1
|
||||||
|
JOIN (
|
||||||
|
SELECT
|
||||||
|
player_id,
|
||||||
|
item_id
|
||||||
|
FROM player_item
|
||||||
|
WHERE is_vip = 0
|
||||||
|
GROUP BY player_id, item_id
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) t2
|
||||||
|
ON t1.player_id = t2.player_id
|
||||||
|
AND t1.item_id = t2.item_id
|
||||||
|
WHERE t1.is_vip = 0
|
||||||
|
ORDER BY t1.player_id, t1.item_id, t1."createTime";
|
||||||
71
help/查询多余精灵.sql
Normal file
71
help/查询多余精灵.sql
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
-- 验证:查询宠物ID为273/274、玩家拥有≥2条且非最早创建的记录(只查不删)
|
||||||
|
WITH player_pet_ranked AS (
|
||||||
|
SELECT
|
||||||
|
id, -- 记录主键ID
|
||||||
|
player_id,
|
||||||
|
(data->>'ID')::INT AS pet_id, -- 便于核对宠物ID
|
||||||
|
"createTime",
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY player_id
|
||||||
|
ORDER BY "createTime" ASC
|
||||||
|
) AS rn
|
||||||
|
FROM "player_pet"
|
||||||
|
WHERE (data->>'ID')::INT IN (273, 274) -- 改为273或274
|
||||||
|
)
|
||||||
|
SELECT *
|
||||||
|
FROM player_pet_ranked
|
||||||
|
WHERE rn > 1 -- rn>1是要删除的记录
|
||||||
|
AND player_id IN (
|
||||||
|
SELECT player_id
|
||||||
|
FROM player_pet_ranked
|
||||||
|
GROUP BY player_id
|
||||||
|
HAVING COUNT(*) >= 2
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- 核心删除:保留每个玩家下273/274宠物中createTime最早的那条,删除其余
|
||||||
|
WITH player_pet_ranked AS (
|
||||||
|
SELECT
|
||||||
|
id, -- 必须取主键ID用于精准删除
|
||||||
|
player_id,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY player_id
|
||||||
|
ORDER BY "createTime" ASC
|
||||||
|
) AS rn
|
||||||
|
FROM "player_pet"
|
||||||
|
WHERE (data->>'ID')::INT IN (273, 274) -- 关键修改:70→273,274
|
||||||
|
)
|
||||||
|
DELETE FROM "player_pet"
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT id
|
||||||
|
FROM player_pet_ranked
|
||||||
|
WHERE rn > 1 -- 删除非最早的记录
|
||||||
|
AND player_id IN (
|
||||||
|
SELECT player_id
|
||||||
|
FROM player_pet_ranked
|
||||||
|
GROUP BY player_id
|
||||||
|
HAVING COUNT(*) >= 2 -- 仅处理拥有≥2条273/274宠物的玩家
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- 查询:player_id=136144、宠物ID=303的所有未删除记录
|
||||||
|
SELECT
|
||||||
|
id, -- 记录主键ID
|
||||||
|
player_id,
|
||||||
|
(data->>'ID')::INT AS pet_id, -- 宠物ID(便于核对)
|
||||||
|
"createTime", -- 创建时间
|
||||||
|
deleted_at, -- 软删除字段(确认是否未删除)
|
||||||
|
data -- 完整的宠物数据(可选,如需查看全部信息)
|
||||||
|
FROM "player_pet"
|
||||||
|
WHERE
|
||||||
|
player_id = 136144 -- 精准定位玩家ID=136144
|
||||||
|
AND (data->>'ID')::INT = 301 -- 精准定位宠物ID=303
|
||||||
|
AND deleted_at IS NULL; -- 仅查未被软删除的记录(如需包含删除的,可删除此条件)
|
||||||
8
help/查询超规精灵.sql
Normal file
8
help/查询超规精灵.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-- 所有宠物ID + 对应记录总数(去重前)
|
||||||
|
SELECT
|
||||||
|
(data->>'ID')::INT AS pet_id, -- 宠物ID
|
||||||
|
COUNT(*) AS total_records -- 该宠物的总持有记录数
|
||||||
|
FROM "player_pet"
|
||||||
|
WHERE data->>'ID' IS NOT NULL -- 过滤无宠物ID的无效记录
|
||||||
|
GROUP BY (data->>'ID')::INT
|
||||||
|
ORDER BY total_records DESC, pet_id ASC; -- 按数量降序、ID升序排列
|
||||||
42
help/约束类.sql
Normal file
42
help/约束类.sql
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
-- 玩家+物品+VIP状态 联合唯一
|
||||||
|
ALTER TABLE player_item
|
||||||
|
ADD CONSTRAINT uk_player_item_player_item_vip
|
||||||
|
UNIQUE (player_id, item_id, is_vip);
|
||||||
|
|
||||||
|
|
||||||
|
-- 玩家+挖矿 联合唯一
|
||||||
|
CREATE UNIQUE INDEX uk_talk_player ON player_talk (talk_id, player_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- 玩家+任务 联合唯一
|
||||||
|
CREATE UNIQUE INDEX uk_player_task ON player_task (player_id, task_id);
|
||||||
|
-- 玩家+称号 联合唯一
|
||||||
|
CREATE UNIQUE INDEX uk_player_title ON player_title (player_id, is_vip) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- 玩家+精灵 联合唯一
|
||||||
|
CREATE UNIQUE INDEX uk_player_pet ON player_pet (player_id, is_vip, catch_time) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- 玩家+CDK 联合唯一
|
||||||
|
CREATE UNIQUE INDEX uk_player_cdk_log
|
||||||
|
ON player_cdk_log (player_id, code_id, is_vip)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
|
||||||
|
-- 玩家孵蛋 联合唯一
|
||||||
|
CREATE UNIQUE INDEX uk_player_egg
|
||||||
|
ON player_egg (player_id, is_vip)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
---PVP索引
|
||||||
|
CREATE UNIQUE INDEX uk_player_pvp
|
||||||
|
ON player_pvp (player_id, season)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
--签到
|
||||||
|
CREATE UNIQUE INDEX uk_player_sign_in_log
|
||||||
|
ON player_sign_in_log (player_id, sign_in_id, is_vip)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
--房间索引
|
||||||
|
CREATE UNIQUE INDEX uk_player_room_house
|
||||||
|
ON player_room_house (player_id, is_vip)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
18
help/诊断较慢.sql
Normal file
18
help/诊断较慢.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
-- 查看当前活跃的 SQL(排查慢查询)
|
||||||
|
SELECT pid, now() - query_start AS duration, query
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE state = 'active' AND now() - query_start > '5 seconds'::interval;
|
||||||
|
|
||||||
|
-- 查看表的访问统计(找出热点表)
|
||||||
|
SELECT relname, seq_scan, idx_scan, n_live_tup
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
ORDER BY seq_scan DESC LIMIT 10;
|
||||||
|
|
||||||
|
-- 查看索引使用情况(找出未使用的索引)
|
||||||
|
SELECT relname AS table_name, indexrelname AS index_name, idx_scan
|
||||||
|
FROM pg_stat_user_indexes
|
||||||
|
WHERE idx_scan = 0;
|
||||||
|
|
||||||
|
-- 更新统计信息(当执行计划不准时)
|
||||||
|
ANALYZE users; -- 单表更新
|
||||||
|
ANALYZE; -- 全库更新
|
||||||
59
help/随机id生成
Normal file
59
help/随机id生成
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
-- 清理旧的
|
||||||
|
ALTER TABLE base_sys_user
|
||||||
|
ALTER COLUMN id SET DEFAULT nextval('base_sys_user_id_seq');
|
||||||
|
DROP FUNCTION IF EXISTS shuffle_9digit() CASCADE;
|
||||||
|
|
||||||
|
-- 9位乱序:首尾互换 + 中间全部打乱
|
||||||
|
-- 纯数字换位,100% 不重复,速度极快
|
||||||
|
CREATE OR REPLACE FUNCTION shuffle_9digit()
|
||||||
|
RETURNS bigint
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
VOLATILE
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
seq bigint;
|
||||||
|
d0 int; -- 原最后一位
|
||||||
|
d1 int;
|
||||||
|
d2 int;
|
||||||
|
d3 int;
|
||||||
|
d4 int;
|
||||||
|
d5 int;
|
||||||
|
d6 int;
|
||||||
|
d7 int; -- 原第一位
|
||||||
|
BEGIN
|
||||||
|
seq := nextval('base_sys_user_id_seq'); -- 8位自增
|
||||||
|
|
||||||
|
-- 拆分 8 位:d7 d6 d5 d4 d3 d2 d1 d0
|
||||||
|
d7 := (seq / 10000000) % 10;
|
||||||
|
d6 := (seq / 1000000) % 10;
|
||||||
|
d5 := (seq / 100000) % 10;
|
||||||
|
d4 := (seq / 10000) % 10;
|
||||||
|
d3 := (seq / 1000) % 10;
|
||||||
|
d2 := (seq / 100) % 10;
|
||||||
|
d1 := (seq / 10) % 10;
|
||||||
|
d0 := seq % 10;
|
||||||
|
|
||||||
|
-- 构造成 9 位,规则:
|
||||||
|
-- 1. 首尾互换(原最后一位放第1位,原第1位放最后)
|
||||||
|
-- 2. 中间全部打乱位置
|
||||||
|
RETURN
|
||||||
|
d0 * 100000000 + -- 原最后一位 → 第1位
|
||||||
|
d3 * 10000000 +
|
||||||
|
d1 * 1000000 +
|
||||||
|
d5 * 100000 +
|
||||||
|
d2 * 10000 +
|
||||||
|
d6 * 1000 +
|
||||||
|
d4 * 100 +
|
||||||
|
d7 * 10 +
|
||||||
|
d0; -- 原第一位 → 第9位
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- 启用
|
||||||
|
ALTER TABLE base_sys_user
|
||||||
|
ALTER COLUMN id SET DEFAULT shuffle_9digit();
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE base_sys_user
|
||||||
|
ALTER COLUMN id
|
||||||
|
SET DEFAULT nextval('base_sys_user_id_seq');
|
||||||
@@ -4,30 +4,36 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"blazing/common/rpc"
|
||||||
"blazing/cool"
|
"blazing/cool"
|
||||||
"blazing/logic/service/common"
|
"blazing/logic/service/common"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/os/glog"
|
"github.com/gogf/gf/v2/os/glog"
|
||||||
"github.com/lunixbochs/struc"
|
"github.com/lunixbochs/struc"
|
||||||
|
"github.com/panjf2000/gnet/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Maincontroller 是控制器层共享变量。
|
||||||
var Maincontroller = &Controller{} //注入service
|
var Maincontroller = &Controller{} //注入service
|
||||||
|
|
||||||
// Controller 分发cmd逻辑实现
|
// Controller 分发cmd逻辑实现
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
Port uint16
|
UID uint32
|
||||||
RPCClient struct {
|
RPCClient *struct {
|
||||||
Kick func(uint32) error
|
Kick func(uint32) error
|
||||||
|
|
||||||
RegisterLogic func(uint16, uint16) error
|
RegisterLogic func(uint32, uint32) error
|
||||||
|
|
||||||
|
MatchJoinOrUpdate func(rpc.PVPMatchJoinPayload) error
|
||||||
|
|
||||||
|
MatchCancel func(uint32) error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,159 +44,198 @@ type Controller struct {
|
|||||||
func ParseCmd[T any](data []byte) T {
|
func ParseCmd[T any](data []byte) T {
|
||||||
var result T
|
var result T
|
||||||
// 使用struc.Unpack将字节数据解包到result变量中
|
// 使用struc.Unpack将字节数据解包到result变量中
|
||||||
struc.Unpack(bytes.NewBuffer(data), &result)
|
struc.Unpack(bytes.NewReader(data), &result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init 初始化控制器,注册所有cmd处理方法
|
// Init 初始化控制器,注册所有cmd处理方法
|
||||||
// 参数 isGame: 标识是否为游戏服务器(true)或登录服务器(false)
|
// 参数 isGame: 标识是否为游戏服务器(true)或登录服务器(false)
|
||||||
func Init(isGame bool) {
|
func Init(isGame bool) {
|
||||||
// 获取控制器实例的反射值
|
|
||||||
controllerValue := reflect.ValueOf(Maincontroller)
|
controllerValue := reflect.ValueOf(Maincontroller)
|
||||||
|
|
||||||
// 获取控制器类型
|
|
||||||
controllerType := controllerValue.Type()
|
controllerType := controllerValue.Type()
|
||||||
|
|
||||||
// 遍历控制器的所有方法
|
|
||||||
for i := 0; i < controllerType.NumMethod(); i++ {
|
for i := 0; i < controllerType.NumMethod(); i++ {
|
||||||
method := controllerType.Method(i)
|
method := controllerType.Method(i)
|
||||||
methodValue := controllerValue.MethodByName(method.Name)
|
methodValue := controllerValue.Method(i)
|
||||||
|
methodType := methodValue.Type()
|
||||||
|
|
||||||
// 获取方法第一个参数的类型(请求结构体)
|
if methodType.NumIn() == 0 {
|
||||||
if methodValue.Type().NumIn() == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析请求结构体中的cmd标签
|
reqArgType := methodType.In(0)
|
||||||
for _, cmd := range getCmd(methodValue.Type().In(0)) {
|
if reqArgType.Kind() != reflect.Ptr || reqArgType.Elem().Kind() != reflect.Struct {
|
||||||
if cmd == 0 { // 说明不是有效的注册方法
|
glog.Warning(context.Background(), "方法首参必须为结构体指针", method.Name, "跳过注册")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reqType := reqArgType.Elem()
|
||||||
|
binding := getCmdBinding(reqType)
|
||||||
|
|
||||||
|
for _, cmd := range binding.cmds {
|
||||||
|
if cmd == 0 {
|
||||||
glog.Warning(context.Background(), "方法参数必须包含CMD参数", method.Name, "跳过注册")
|
glog.Warning(context.Background(), "方法参数必须包含CMD参数", method.Name, "跳过注册")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据服务器类型过滤cmd
|
if methodType.NumIn() != 2 {
|
||||||
// 登录服务器只处理小于1000的cmd
|
glog.Warning(context.Background(), "方法参数数量必须为2", method.Name, "跳过注册")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if !isGame && cmd > 1000 {
|
if !isGame && cmd > 1000 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 游戏服务器只处理大于等于1000的cmd
|
|
||||||
if isGame && cmd < 1000 {
|
if isGame && cmd < 1000 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册命令处理函数
|
|
||||||
|
|
||||||
if cool.Config.ServerInfo.IsDebug != 0 {
|
if cool.Config.ServerInfo.IsDebug != 0 {
|
||||||
fmt.Println("注册方法", cmd, method.Name)
|
fmt.Println("注册方法", cmd, method.Name)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reqTypeForNew := reqType
|
||||||
cmdInfo := cool.Cmd{
|
cmdInfo := cool.Cmd{
|
||||||
Func: methodValue,
|
Func: methodValue,
|
||||||
Req: methodValue.Type().In(0).Elem(),
|
Req: reqType,
|
||||||
// Res: , // TODO 待实现对不同用户初始化方法以取消全局cmdcache
|
HeaderFieldIndex: append([]int(nil), binding.headerFieldIndex...),
|
||||||
|
UseConn: methodType.In(1) == connType,
|
||||||
|
NewReqFunc: func() interface{} {
|
||||||
|
return reflect.New(reqTypeForNew).Interface()
|
||||||
|
},
|
||||||
|
NewReqValue: func() reflect.Value {
|
||||||
|
return reflect.New(reqTypeForNew)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, exists := cool.CmdCache[cmd]; exists {
|
||||||
|
panic(fmt.Sprintf("命令处理方法已存在,跳过注册 %d %s", cmd, method.Name))
|
||||||
|
}
|
||||||
cool.CmdCache[cmd] = cmdInfo
|
cool.CmdCache[cmd] = cmdInfo
|
||||||
// if exists { // 方法已存在
|
|
||||||
// glog.Error(context.Background(), "命令处理方法已存在,跳过注册", cmd, method.Name)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetType = reflect.TypeOf(common.TomeeHeader{})
|
var (
|
||||||
|
targetType = reflect.TypeOf(common.TomeeHeader{})
|
||||||
|
connType = reflect.TypeOf((*gnet.Conn)(nil)).Elem()
|
||||||
|
cmdTypeCache sync.Map
|
||||||
|
)
|
||||||
|
|
||||||
// 默认返回值(无匹配字段/解析失败时)
|
// 默认返回值(无匹配字段/解析失败时)
|
||||||
const defaultCmdValue = 0
|
const defaultCmdValue = 0
|
||||||
|
|
||||||
// getCmd 从结构体类型中提取绑定的cmd指令(递归查找嵌套结构体,支持值/指针类型的TomeeHeader)
|
type cmdBinding struct {
|
||||||
// 参数 typ: 待解析的结构体类型(支持多层指针)
|
cmds []uint32
|
||||||
// 返回值: 解析到的cmd切片,无匹配/解析失败时返回[defaultCmdValue]
|
headerFieldIndex []int
|
||||||
func getCmd(typ reflect.Type) []uint32 {
|
}
|
||||||
// 递归解引用所有指针类型(处理 *struct、**struct 等场景)
|
|
||||||
|
func normalizeStructType(typ reflect.Type) reflect.Type {
|
||||||
for typ.Kind() == reflect.Ptr {
|
for typ.Kind() == reflect.Ptr {
|
||||||
typ = typ.Elem()
|
typ = typ.Elem()
|
||||||
}
|
}
|
||||||
|
return typ
|
||||||
|
}
|
||||||
|
|
||||||
// 非结构体类型直接返回默认值
|
// getCmdBinding 从结构体类型中提取绑定的cmd指令和头字段位置。
|
||||||
if typ.Kind() != reflect.Struct {
|
func getCmdBinding(typ reflect.Type) cmdBinding {
|
||||||
return []uint32{defaultCmdValue}
|
typ = normalizeStructType(typ)
|
||||||
|
if cached, ok := cmdTypeCache.Load(typ); ok {
|
||||||
|
return cached.(cmdBinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 遍历结构体字段,查找TomeeHeader字段并解析cmd
|
if typ.Kind() != reflect.Struct {
|
||||||
|
binding := cmdBinding{cmds: []uint32{defaultCmdValue}}
|
||||||
|
cmdTypeCache.Store(typ, binding)
|
||||||
|
return binding
|
||||||
|
}
|
||||||
|
|
||||||
|
if binding, ok := findCmdBinding(typ, make(map[reflect.Type]struct{})); ok {
|
||||||
|
cmdTypeCache.Store(typ, binding)
|
||||||
|
return binding
|
||||||
|
}
|
||||||
|
|
||||||
|
binding := cmdBinding{cmds: []uint32{defaultCmdValue}}
|
||||||
|
cmdTypeCache.Store(typ, binding)
|
||||||
|
return binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCmdBinding(typ reflect.Type, visiting map[reflect.Type]struct{}) (cmdBinding, bool) {
|
||||||
|
typ = normalizeStructType(typ)
|
||||||
|
if typ.Kind() != reflect.Struct {
|
||||||
|
return cmdBinding{}, false
|
||||||
|
}
|
||||||
|
if _, seen := visiting[typ]; seen {
|
||||||
|
return cmdBinding{}, false
|
||||||
|
}
|
||||||
|
visiting[typ] = struct{}{}
|
||||||
|
defer delete(visiting, typ)
|
||||||
|
|
||||||
for i := 0; i < typ.NumField(); i++ {
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
field := typ.Field(i)
|
field := typ.Field(i)
|
||||||
|
|
||||||
// 尝试解析当前字段的cmd标签
|
cmdSlice, isHeader, err := parseCmdTagWithStructField(field)
|
||||||
cmdSlice, err := parseCmdTagWithStructField(field)
|
if isHeader && err == nil {
|
||||||
if err == nil { // 解析成功,直接返回结果
|
return cmdBinding{
|
||||||
return cmdSlice
|
cmds: cmdSlice,
|
||||||
|
headerFieldIndex: append([]int(nil), field.Index...),
|
||||||
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归处理嵌套结构体(值/指针类型)
|
nestedTyp := normalizeStructType(field.Type)
|
||||||
nestedTyp := field.Type
|
if nestedTyp.Kind() != reflect.Struct {
|
||||||
if nestedTyp.Kind() == reflect.Ptr {
|
continue
|
||||||
nestedTyp = nestedTyp.Elem()
|
|
||||||
}
|
}
|
||||||
if nestedTyp.Kind() == reflect.Struct {
|
|
||||||
// 递归查找,找到有效cmd则立即返回
|
nestedBinding, ok := findCmdBinding(nestedTyp, visiting)
|
||||||
if nestedCmd := getCmd(nestedTyp); len(nestedCmd) > 0 && nestedCmd[0] != defaultCmdValue {
|
if !ok {
|
||||||
return nestedCmd
|
continue
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldIndex := make([]int, 0, len(field.Index)+len(nestedBinding.headerFieldIndex))
|
||||||
|
fieldIndex = append(fieldIndex, field.Index...)
|
||||||
|
fieldIndex = append(fieldIndex, nestedBinding.headerFieldIndex...)
|
||||||
|
nestedBinding.headerFieldIndex = fieldIndex
|
||||||
|
return nestedBinding, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未找到目标字段/所有解析失败,返回默认值
|
return cmdBinding{}, false
|
||||||
return []uint32{defaultCmdValue}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseCmdTagWithStructField 校验字段是否为TomeeHeader(值/指针)并解析cmd标签
|
// parseCmdTagWithStructField 校验字段是否为TomeeHeader(值/指针)并解析cmd标签
|
||||||
// 参数 field: 结构体字段元信息
|
// 参数 field: 结构体字段元信息
|
||||||
// 返回值: 解析后的cmd切片,非目标类型/解析失败返回错误
|
// 返回值: 解析后的cmd切片,是否为目标类型,解析失败错误
|
||||||
func parseCmdTagWithStructField(field reflect.StructField) ([]uint32, error) {
|
func parseCmdTagWithStructField(field reflect.StructField) ([]uint32, bool, error) {
|
||||||
// 判断字段类型是否为 TomeeHeader 或 *TomeeHeader
|
if field.Type != targetType && !(field.Type.Kind() == reflect.Ptr && field.Type.Elem() == targetType) {
|
||||||
var isTomeeHeader bool
|
return nil, false, nil
|
||||||
switch {
|
|
||||||
case field.Type == targetType: // 值类型
|
|
||||||
isTomeeHeader = true
|
|
||||||
case field.Type.Kind() == reflect.Ptr && field.Type.Elem() == targetType: // 指针类型
|
|
||||||
isTomeeHeader = true
|
|
||||||
default:
|
|
||||||
isTomeeHeader = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非目标类型返回错误
|
|
||||||
if !isTomeeHeader {
|
|
||||||
return nil, fmt.Errorf("field %s (type: %v) is not common.TomeeHeader or *common.TomeeHeader",
|
|
||||||
field.Name, field.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取cmd标签
|
|
||||||
cmdStr := field.Tag.Get("cmd")
|
cmdStr := field.Tag.Get("cmd")
|
||||||
if cmdStr == "" {
|
if cmdStr == "" {
|
||||||
return nil, fmt.Errorf("field %s cmd tag is empty", field.Name)
|
return nil, true, fmt.Errorf("field %s cmd tag is empty", field.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 高性能解析标签为uint32切片(替代gconv,减少第三方依赖且可控)
|
result := make([]uint32, 0, strings.Count(cmdStr, "|")+1)
|
||||||
parts := strings.Split(cmdStr, "|")
|
remain := cmdStr
|
||||||
result := make([]uint32, 0, len(parts))
|
for idx := 0; ; idx++ {
|
||||||
for idx, s := range parts {
|
part, next, found := strings.Cut(remain, "|")
|
||||||
// 去除空白字符(兼容标签中意外的空格)
|
s := strings.TrimSpace(part)
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return nil, fmt.Errorf("field %s cmd tag part %d is empty", field.Name, idx)
|
return nil, true, fmt.Errorf("field %s cmd tag part %d is empty", field.Name, idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手动解析uint32,比gconv更可控,避免隐式转换问题
|
|
||||||
num, err := strconv.ParseUint(s, 10, 32)
|
num, err := strconv.ParseUint(s, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("field %s cmd tag part %d parse error: %v (value: %s)",
|
return nil, true, fmt.Errorf("field %s cmd tag part %d parse error: %v (value: %s)",
|
||||||
field.Name, idx, err, s)
|
field.Name, idx, err, s)
|
||||||
}
|
}
|
||||||
result = append(result, uint32(num))
|
result = append(result, uint32(num))
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
remain = next
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
177
logic/controller/action_大师杯.go
Normal file
177
logic/controller/action_大师杯.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/data"
|
||||||
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/logic/service/common"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
"blazing/logic/service/task"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
|
||||||
|
"github.com/pointernil/bitset32"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
masterCupTaskID uint32 = 111
|
||||||
|
masterCupRewardItemMin uint32 = 80000000
|
||||||
|
masterCupRewardItemMax uint32 = 80000015
|
||||||
|
)
|
||||||
|
|
||||||
|
var masterCupRewardElementOrder = [...]uint32{1, 2, 3, 5, 11, 4, 6, 7, 9}
|
||||||
|
|
||||||
|
var masterCupRequiredItems = map[uint32][]ItemS{
|
||||||
|
8: {
|
||||||
|
{ItemId: 80000001, ItemCnt: 100},
|
||||||
|
{ItemId: 80000002, ItemCnt: 20},
|
||||||
|
{ItemId: 80000003, ItemCnt: 20},
|
||||||
|
{ItemId: 80000005, ItemCnt: 20},
|
||||||
|
{ItemId: 80000011, ItemCnt: 20},
|
||||||
|
},
|
||||||
|
7: {
|
||||||
|
{ItemId: 80000001, ItemCnt: 20},
|
||||||
|
{ItemId: 80000002, ItemCnt: 20},
|
||||||
|
{ItemId: 80000003, ItemCnt: 100},
|
||||||
|
{ItemId: 80000005, ItemCnt: 20},
|
||||||
|
{ItemId: 80000011, ItemCnt: 20},
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
{ItemId: 80000001, ItemCnt: 20},
|
||||||
|
{ItemId: 80000002, ItemCnt: 100},
|
||||||
|
{ItemId: 80000003, ItemCnt: 20},
|
||||||
|
{ItemId: 80000005, ItemCnt: 20},
|
||||||
|
{ItemId: 80000011, ItemCnt: 20},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// DASHIbei 处理控制器请求。
|
||||||
|
func (h Controller) DASHIbei(req *C2s_MASTER_REWARDS, c *player.Player) (result *S2C_MASTER_REWARDS, err errorcode.ErrorCode) {
|
||||||
|
_ = req
|
||||||
|
result = &S2C_MASTER_REWARDS{}
|
||||||
|
items := c.Service.Item.Get(masterCupRewardItemMin, masterCupRewardItemMax)
|
||||||
|
result.Reward = buildMasterCupRewards(items)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DASHIbeiR 处理控制器请求。
|
||||||
|
func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (result *S2C_MASTER_REWARDSR, err errorcode.ErrorCode) {
|
||||||
|
result = &S2C_MASTER_REWARDSR{}
|
||||||
|
|
||||||
|
requiredItems, ok := masterCupRequiredItems[req.ElementType]
|
||||||
|
if !ok {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
|
}
|
||||||
|
|
||||||
|
taskInfo := task.GetTaskInfo(int(masterCupTaskID), int(req.ElementType))
|
||||||
|
if taskInfo == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrNeedCompleteTaskForPrize
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasEnoughMasterCupItems(c, requiredItems) {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrInsufficientItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ItemList = make([]data.ItemInfo, 0, len(taskInfo.ItemList))
|
||||||
|
taskData, taskErr := c.Service.Task.GetTask(masterCupTaskID)
|
||||||
|
if taskErr != nil {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
|
}
|
||||||
|
|
||||||
|
progress := bitset32.From(taskData.Data)
|
||||||
|
if progress.Test(uint(req.ElementType)) {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := consumeMasterCupItems(c, requiredItems); err != nil {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrInsufficientItems)
|
||||||
|
}
|
||||||
|
progress.Set(uint(req.ElementType))
|
||||||
|
taskData.Data = progress.Bytes()
|
||||||
|
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if taskInfo.Pet != nil {
|
||||||
|
c.Service.Pet.PetAdd(taskInfo.Pet, 0)
|
||||||
|
result.CaptureTime = taskInfo.Pet.CatchTime
|
||||||
|
result.PetTypeId = taskInfo.Pet.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
appendMasterCupRewardItems(c, result, taskInfo.ItemList)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemS 定义请求或响应数据结构。
|
||||||
|
type ItemS struct {
|
||||||
|
ItemId uint32
|
||||||
|
ItemCnt uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMasterCupRewards(items []model.Item) []uint32 {
|
||||||
|
itemCounts := make(map[uint32]uint32, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
itemCounts[item.ItemId] = uint32(item.ItemCnt)
|
||||||
|
}
|
||||||
|
|
||||||
|
rewards := make([]uint32, len(masterCupRewardElementOrder))
|
||||||
|
for i, elementType := range masterCupRewardElementOrder {
|
||||||
|
rewards[i] = itemCounts[masterCupRewardItemMin+elementType]
|
||||||
|
}
|
||||||
|
|
||||||
|
return rewards
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasEnoughMasterCupItems(c *player.Player, requiredItems []ItemS) bool {
|
||||||
|
for _, item := range requiredItems {
|
||||||
|
if c.Service.Item.CheakItem(item.ItemId) < int64(item.ItemCnt) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) error {
|
||||||
|
for _, item := range requiredItems {
|
||||||
|
if err := c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendMasterCupRewardItems(c *player.Player, result *S2C_MASTER_REWARDSR, itemList []data.ItemInfo) {
|
||||||
|
for _, item := range itemList {
|
||||||
|
if c.ItemAdd(item.ItemId, item.ItemCnt) {
|
||||||
|
result.ItemList = append(result.ItemList, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2s_MASTER_REWARDS 定义请求或响应数据结构。
|
||||||
|
type C2s_MASTER_REWARDS struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2611" struc:"skip"` //玩家登录
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutInfo 表示地图热度的出站消息
|
||||||
|
type S2C_MASTER_REWARDS struct {
|
||||||
|
ReLen uint32 `struc:"sizeof=Reward"`
|
||||||
|
Reward []uint32 `json:"Reward"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2s_MASTER_REWARDSR 定义请求或响应数据结构。
|
||||||
|
type C2s_MASTER_REWARDSR struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2612" struc:"skip"` //玩家登录
|
||||||
|
ElementType uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutInfo 表示地图热度的出站消息
|
||||||
|
type S2C_MASTER_REWARDSR struct {
|
||||||
|
BounsID uint32
|
||||||
|
PetTypeId uint32
|
||||||
|
CaptureTime uint32
|
||||||
|
|
||||||
|
ItemListLen uint32 `struc:"sizeof=ItemList"`
|
||||||
|
ItemList []data.ItemInfo `json:"Reward"`
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"blazing/common/data"
|
"blazing/common/data"
|
||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
"blazing/logic/service/egg"
|
"blazing/logic/service/common"
|
||||||
|
|
||||||
"blazing/logic/service/player"
|
"blazing/logic/service/player"
|
||||||
"blazing/modules/config/service"
|
"blazing/modules/config/service"
|
||||||
"blazing/modules/player/model"
|
"blazing/modules/player/model"
|
||||||
@@ -11,7 +12,8 @@ import (
|
|||||||
"github.com/gogf/gf/v2/util/grand"
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h Controller) EggGamePlay(data1 *egg.C2S_EGG_GAME_PLAY, c *player.Player) (result *egg.S2C_EGG_GAME_PLAY, err errorcode.ErrorCode) {
|
// EggGamePlay 处理控制器请求。
|
||||||
|
func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (result *S2C_EGG_GAME_PLAY, err errorcode.ErrorCode) {
|
||||||
|
|
||||||
switch data1.EggNum {
|
switch data1.EggNum {
|
||||||
case 2:
|
case 2:
|
||||||
@@ -21,33 +23,52 @@ func (h Controller) EggGamePlay(data1 *egg.C2S_EGG_GAME_PLAY, c *player.Player)
|
|||||||
data1.EggNum = 10
|
data1.EggNum = 10
|
||||||
}
|
}
|
||||||
r := c.Service.Item.CheakItem(400501)
|
r := c.Service.Item.CheakItem(400501)
|
||||||
if r < int32(data1.EggNum) {
|
if data1.EggNum > 10 || data1.EggNum <= 0 {
|
||||||
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
|
|
||||||
}
|
}
|
||||||
result = &egg.S2C_EGG_GAME_PLAY{ListInfo: []data.ItemInfo{}}
|
if r <= 0 || data1.EggNum > r {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
|
||||||
|
}
|
||||||
|
if err := c.Service.Item.UPDATE(400501, int(-data1.EggNum)); err != nil {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
|
||||||
|
}
|
||||||
|
result = &S2C_EGG_GAME_PLAY{ListInfo: []data.ItemInfo{}}
|
||||||
if grand.Meet(int(data1.EggNum), 100) {
|
if grand.Meet(int(data1.EggNum), 100) {
|
||||||
r := service.NewPetRewardService().GetEgg()
|
r := service.NewPetRewardService().GetEgg()
|
||||||
newPet := model.GenPetInfo(int(r.MonID), int(r.DV), int(r.Nature), int(r.Effect), int(r.Lv), nil)
|
newPet := model.GenPetInfo(int(r.MonID), int(r.DV), int(r.Nature), int(r.Effect), int(r.Lv), nil, 0)
|
||||||
if grand.Meet(int(data1.EggNum), 100) {
|
if grand.Meet(1, 500) {
|
||||||
newPet.RandShiny()
|
newPet.RandomByWeightShiny()
|
||||||
}
|
}
|
||||||
c.Service.Pet.PetAdd(newPet)
|
|
||||||
|
c.Service.Pet.PetAdd(newPet, 0)
|
||||||
|
|
||||||
result.HadTime = newPet.CatchTime
|
result.HadTime = newPet.CatchTime
|
||||||
result.PetID = newPet.ID
|
result.PetID = newPet.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
items := service.NewItemService().GetEgg(data1.EggNum)
|
items := service.NewItemService().GetEgg(int(data1.EggNum))
|
||||||
for _, item := range items {
|
addedItems := c.ItemAddBatch(items)
|
||||||
if item.ItemId == 0 {
|
for _, item := range addedItems {
|
||||||
continue
|
|
||||||
}
|
|
||||||
c.ItemAdd(item.ItemId, item.ItemCnt)
|
|
||||||
result.ListInfo = append(result.ListInfo, data.ItemInfo{ItemId: item.ItemId, ItemCnt: item.ItemCnt})
|
result.ListInfo = append(result.ListInfo, data.ItemInfo{ItemId: item.ItemId, ItemCnt: item.ItemCnt})
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Service.Item.UPDATE(400501, int(-data1.EggNum))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// C2S_EGG_GAME_PLAY 前端向后端发送的抽蛋请求结构体
|
||||||
|
// 对应原 C# 的 C2S_EGG_GAME_PLAY
|
||||||
|
type C2S_EGG_GAME_PLAY struct {
|
||||||
|
Head common.TomeeHeader `cmd:"3201" struc:"skip"`
|
||||||
|
EggNum int64 `struc:"uint32"` // 抽蛋次数标识:1 = 1次 2 = 5次 3 = 10次
|
||||||
|
// 注:Go 中 uint 是平台相关类型(32/64位),游戏开发中推荐用 uint32 明确匹配 C# 的 uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// S2C_EGG_GAME_PLAY 后端向前端返回的抽蛋结果结构体
|
||||||
|
// 对应原 C# 的 S2C_EGG_GAME_PLAY
|
||||||
|
type S2C_EGG_GAME_PLAY struct {
|
||||||
|
GiftIN uint32 `struc:"uint32"`
|
||||||
|
PetID uint32 `struc:"uint32"` // 抽中精灵的id
|
||||||
|
HadTime uint32 `struc:"uint32"` // 抽中精灵的捕捉时间(若为时间戳,建议改为 uint64)
|
||||||
|
ListInfoLen uint32 `struc:"sizeof=ListInfo"`
|
||||||
|
ListInfo []data.ItemInfo `json:"listinfo"` // 抽中物品的物品数组
|
||||||
|
}
|
||||||
|
|||||||
87
logic/controller/action_炫彩碎片.go
Normal file
87
logic/controller/action_炫彩碎片.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/logic/service/common"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw15To10WithBitSet 15抽10,返回标记抽取结果的uint32(位1表示选中)
|
||||||
|
// 规则:uint32的第n位(0≤n≤14)=1 → 选中第n+1号元素
|
||||||
|
func Draw15To10WithBitSet() uint32 {
|
||||||
|
// 初始化随机数生成器
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
var resultBits uint32 // 核心结果:用位标记选中的元素
|
||||||
|
selectedCount := 0 // 已选中的数量
|
||||||
|
|
||||||
|
// 循环直到选中10个元素
|
||||||
|
for selectedCount < 10 {
|
||||||
|
// 随机生成0~14的位索引(对应1~15号元素)
|
||||||
|
randBitIdx := r.Intn(15)
|
||||||
|
// 构造掩码:仅第randBitIdx位为1
|
||||||
|
mask := uint32(1) << randBitIdx
|
||||||
|
|
||||||
|
// 检查该位是否未被选中(避免重复)
|
||||||
|
if (resultBits & mask) == 0 {
|
||||||
|
resultBits |= mask // 标记该位为选中
|
||||||
|
selectedCount++ // 选中数+1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultBits
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET_XUANCAI 处理控制器请求。
|
||||||
|
func (h Controller) GET_XUANCAI(data *C2s_GET_XUANCAI, c *player.Player) (result *S2C_GET_XUANCAI, err errorcode.ErrorCode) {
|
||||||
|
result = &S2C_GET_XUANCAI{}
|
||||||
|
selectedCount := 0 // 已选中的数量
|
||||||
|
res := c.Info.GetTask(13) //第一期
|
||||||
|
if res == model.Completed {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrDailyGiftLimit)
|
||||||
|
}
|
||||||
|
c.Info.SetTask(13, model.Completed)
|
||||||
|
selectedItems := make([]uint32, 0, 10)
|
||||||
|
itemMask := make(map[uint32]uint32, 10)
|
||||||
|
// 循环直到选中10个元素
|
||||||
|
for selectedCount < 10 {
|
||||||
|
// 随机生成0~14的位索引(对应1~15号元素)
|
||||||
|
randBitIdx := grand.Intn(15)
|
||||||
|
// 构造掩码:仅第randBitIdx位为1
|
||||||
|
mask := uint32(1) << randBitIdx
|
||||||
|
|
||||||
|
// 检查该位是否未被选中(避免重复)
|
||||||
|
if (result.Status & mask) == 0 {
|
||||||
|
result.Status |= mask
|
||||||
|
itemID := uint32(400686 + randBitIdx + 1)
|
||||||
|
selectedItems = append(selectedItems, itemID)
|
||||||
|
itemMask[itemID] = mask
|
||||||
|
selectedCount++ // 选中数+1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
successItems, addErr := c.Service.Item.AddUniqueItems(selectedItems)
|
||||||
|
if addErr != nil {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError200007)
|
||||||
|
}
|
||||||
|
for _, itemID := range successItems {
|
||||||
|
result.Status |= itemMask[itemID]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2s_GET_XUANCAI 定义请求或响应数据结构。
|
||||||
|
type C2s_GET_XUANCAI struct {
|
||||||
|
Head common.TomeeHeader `cmd:"60001" struc:"skip"` //玩家登录
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutInfo 表示地图热度的出站消息
|
||||||
|
type S2C_GET_XUANCAI struct {
|
||||||
|
Status uint32
|
||||||
|
}
|
||||||
53
logic/controller/action_超时空隧道.go
Normal file
53
logic/controller/action_超时空隧道.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/logic/service/common"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
"blazing/modules/config/service"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 进入超时空隧道
|
||||||
|
func (h Controller) TimeMap(data *C2s_SP, c *player.Player) (result *S2C_SP, err errorcode.ErrorCode) {
|
||||||
|
result = &S2C_SP{}
|
||||||
|
maps := service.NewMapService().GetTimeMap()
|
||||||
|
result.MapList = make([]ServerInfo, len(maps))
|
||||||
|
for i, v := range maps {
|
||||||
|
result.MapList[i].ID = v.MapID
|
||||||
|
result.MapList[i].DropItemIds = v.DropItemIds
|
||||||
|
pits := service.NewMapPitService().GetDataALL(v.MapID)
|
||||||
|
|
||||||
|
for _, v := range pits {
|
||||||
|
|
||||||
|
result.MapList[i].Pet = append(result.MapList[i].Pet, v.RefreshID...)
|
||||||
|
|
||||||
|
}
|
||||||
|
result.MapList[i].Pet = lo.Union(result.MapList[i].Pet)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2s_SP 定义请求或响应数据结构。
|
||||||
|
type C2s_SP struct {
|
||||||
|
Head common.TomeeHeader `cmd:"60002" struc:"skip"` //超时空地图
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutInfo 表示地图热度的出站消息
|
||||||
|
type S2C_SP struct {
|
||||||
|
MapListLen uint32 `struc:"sizeof=MapList"`
|
||||||
|
MapList []ServerInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerInfo 定义请求或响应数据结构。
|
||||||
|
type ServerInfo struct {
|
||||||
|
ID uint32 //地图ID
|
||||||
|
PetLen uint32 `struc:"sizeof=Pet"`
|
||||||
|
Pet []uint32 //拥有的精灵
|
||||||
|
DropItemIdsLen uint32 `struc:"sizeof=DropItemIds"`
|
||||||
|
DropItemIds []uint32 //掉落物
|
||||||
|
GameLen uint32 `struc:"sizeof=Game"`
|
||||||
|
Game string
|
||||||
|
}
|
||||||
@@ -2,12 +2,14 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
"blazing/logic/service/leiyi"
|
|
||||||
|
"blazing/logic/service/common"
|
||||||
"blazing/logic/service/player"
|
"blazing/logic/service/player"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h Controller) GetLeiyiTrainStatus(data *leiyi.C2s_LEIYI_TRAIN_GET_STATUS, c *player.Player) (result *leiyi.S2C_LEIYI_TRAIN_GET_STATUS, err errorcode.ErrorCode) {
|
// GetLeiyiTrainStatus 处理控制器请求。
|
||||||
result = &leiyi.S2C_LEIYI_TRAIN_GET_STATUS{}
|
func (h Controller) GetLeiyiTrainStatus(data *C2s_LEIYI_TRAIN_GET_STATUS, c *player.Player) (result *S2C_LEIYI_TRAIN_GET_STATUS, err errorcode.ErrorCode) {
|
||||||
|
result = &S2C_LEIYI_TRAIN_GET_STATUS{}
|
||||||
|
|
||||||
for i := 0; i < 6; i++ {
|
for i := 0; i < 6; i++ {
|
||||||
result.Status[i].Total = 10
|
result.Status[i].Total = 10
|
||||||
@@ -17,3 +19,21 @@ func (h Controller) GetLeiyiTrainStatus(data *leiyi.C2s_LEIYI_TRAIN_GET_STATUS,
|
|||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// C2s_LEIYI_TRAIN_GET_STATUS 定义请求或响应数据结构。
|
||||||
|
type C2s_LEIYI_TRAIN_GET_STATUS struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2393" struc:"skip"` //玩家登录
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutInfo 表示地图热度的出站消息
|
||||||
|
type S2C_LEIYI_TRAIN_GET_STATUS struct {
|
||||||
|
Status [10]S2C_LEIYI_TRAIN_GET_STATUS_info `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// S2C_LEIYI_TRAIN_GET_STATUS_info 定义请求或响应数据结构。
|
||||||
|
type S2C_LEIYI_TRAIN_GET_STATUS_info struct {
|
||||||
|
// Today uint32 // 今日训练HP次数
|
||||||
|
Current uint32 // 当前训练HP次数
|
||||||
|
Total uint32 // 目标训练HP次数
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/cool"
|
||||||
|
"blazing/logic/service/common"
|
||||||
"blazing/logic/service/fight"
|
"blazing/logic/service/fight"
|
||||||
"blazing/logic/service/pet"
|
|
||||||
"blazing/logic/service/player"
|
"blazing/logic/service/player"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,10 +12,30 @@ import (
|
|||||||
// data: 空输入结构
|
// data: 空输入结构
|
||||||
// c: 当前玩家对象
|
// c: 当前玩家对象
|
||||||
// 返回: 捕捉结果(消耗的EV值)和错误码
|
// 返回: 捕捉结果(消耗的EV值)和错误码
|
||||||
func (h Controller) HanLiuQiang(data *pet.C2S_2608, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) HanLiuQiang(data *C2S_2608, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
|
||||||
if c.ItemAdd(100245, 1) {
|
if c.ItemAdd(100245, 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cool.Config.ServerInfo.IsVip == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ItemAdd(500655, 1)
|
||||||
|
|
||||||
|
// pet := model.GenPetInfo(426, 31, -1, -1, 100, nil, 0)
|
||||||
|
|
||||||
|
// c.Service.Pet.PetAdd(pet, 0)
|
||||||
|
// pet = model.GenPetInfo(1567, 31, -1, -1, 100, nil, 0)
|
||||||
|
|
||||||
|
// c.Service.Pet.PetAdd(pet)
|
||||||
|
// pet = model.GenPetInfo(1905, 31, -1, -1, 100, nil, 0)
|
||||||
|
|
||||||
|
// c.Service.Pet.PetAdd(pet)
|
||||||
return result, -1
|
return result, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// C2S_2608 定义请求或响应数据结构。
|
||||||
|
type C2S_2608 struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2608" struc:"skip"`
|
||||||
|
}
|
||||||
|
|||||||
30
logic/controller/buy_seerdou_item.go
Normal file
30
logic/controller/buy_seerdou_item.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/data/xmlres"
|
||||||
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buySeerdouBackpackItem(player *player.Player, itemID int64, count int64) (bought bool, err errorcode.ErrorCode) {
|
||||||
|
if itemID <= 0 || count <= 0 {
|
||||||
|
return false, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
}
|
||||||
|
|
||||||
|
itemInfo, exists := xmlres.ItemsMAP[int(itemID)]
|
||||||
|
if !exists {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCost := int64(itemInfo.Price) * count
|
||||||
|
if totalCost > 0 && !player.GetCoins(totalCost) {
|
||||||
|
return false, errorcode.ErrorCodes.ErrSunDouInsufficient10016
|
||||||
|
}
|
||||||
|
|
||||||
|
if !player.ItemAdd(itemID, count) {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
player.Info.Coins -= totalCost
|
||||||
|
return true, 0
|
||||||
|
}
|
||||||
@@ -17,74 +17,158 @@ func (h Controller) checkFightStatus(c *player.Player) errorcode.ErrorCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OnReadyToFight 准备战斗
|
// OnReadyToFight 准备战斗
|
||||||
func (h Controller) OnReadyToFight(data *fight.ReadyToFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) OnReadyToFight(data *ReadyToFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
if err := h.checkFightStatus(c); err != 0 {
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer c.FightC.ReadyFight(c)
|
go c.FightC.ReadyFight(c)
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupReadyFightFinish 旧组队协议准备完成。
|
||||||
|
func (h Controller) GroupReadyFightFinish(data *GroupReadyFightFinishInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
go c.FightC.ReadyFight(c)
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupUseSkill(data *GroupUseSkillInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targetRelation := fight.SkillTargetOpponent
|
||||||
|
if data.TargetSide == 1 {
|
||||||
|
targetRelation = fight.SkillTargetAlly
|
||||||
|
}
|
||||||
|
h.dispatchFightActionEnvelope(c, fight.NewSkillActionEnvelope(data.SkillId, int(data.ActorIndex), int(data.TargetPos), targetRelation, 0))
|
||||||
|
c.SendPackCmd(7558, nil)
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupUseItem(data *GroupUseItemInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.dispatchFightActionEnvelope(c, fight.NewItemActionEnvelope(0, data.ItemId, int(data.ActorIndex), int(data.ActorIndex), fight.SkillTargetSelf))
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupChangePet(data *GroupChangePetInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.dispatchFightActionEnvelope(c, fight.NewChangeActionEnvelope(data.CatchTime, int(data.ActorIndex)))
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupEscape(data *GroupEscapeInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if fightC, ok := c.FightC.(*fight.FightC); ok && fightC != nil && fightC.LegacyGroupProtocol {
|
||||||
|
fightC.SendLegacyEscapeSuccess(c, int(data.ActorIndex))
|
||||||
|
}
|
||||||
|
h.dispatchFightActionEnvelope(c, fight.NewEscapeActionEnvelope())
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupFightWinClose(data *GroupFightWinCloseInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if c != nil {
|
||||||
|
c.QuitFight()
|
||||||
|
}
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupFightTimeoutExit(data *GroupFightTimeoutExitInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if c != nil {
|
||||||
|
c.QuitFight()
|
||||||
|
}
|
||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseSkill 使用技能包
|
// UseSkill 使用技能包
|
||||||
func (h Controller) UseSkill(data *fight.UseSkillInInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) UseSkill(data *UseSkillInInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
if err := h.checkFightStatus(c); err != 0 {
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer c.FightC.UseSkill(c, data.SkillId)
|
h.dispatchFightActionEnvelope(c, buildLegacyUseSkillEnvelope(data))
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseSkillAt 组队/多战位技能包(cmd=7505)。
|
||||||
|
// 目标关系:0=对方 1=自己 2=队友。
|
||||||
|
func (h Controller) UseSkillAt(data *UseSkillAtInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.dispatchFightActionEnvelope(c, buildIndexedUseSkillEnvelope(data))
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape 战斗逃跑
|
// Escape 战斗逃跑
|
||||||
func (h Controller) Escape(data *fight.EscapeFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) Escape(data *EscapeFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
if err := h.checkFightStatus(c); err != 0 {
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
h.dispatchFightActionEnvelope(c, buildLegacyEscapeEnvelope())
|
||||||
defer c.FightC.Over(c, info.BattleOverReason.PlayerEscape)
|
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangePet 切换精灵
|
// ChangePet 切换精灵
|
||||||
func (h Controller) ChangePet(data *fight.ChangePetInboundInfo, c *player.Player) (result *info.ChangePetInfo, err errorcode.ErrorCode) {
|
func (h Controller) ChangePet(data *ChangePetInboundInfo, c *player.Player) (result *info.ChangePetInfo, err errorcode.ErrorCode) {
|
||||||
if err := h.checkFightStatus(c); err != 0 {
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer c.FightC.ChangePet(c, data.CatchTime)
|
h.dispatchFightActionEnvelope(c, buildLegacyChangeEnvelope(data))
|
||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture 捕捉精灵
|
// Capture 捕捉精灵
|
||||||
func (h Controller) Capture(data *fight.CatchMonsterInboundInfo, c *player.Player) (result *info.CatchMonsterOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) Capture(data *CatchMonsterInboundInfo, c *player.Player) (result *info.CatchMonsterOutboundInfo, err errorcode.ErrorCode) {
|
||||||
if err := h.checkFightStatus(c); err != 0 {
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer c.FightC.Capture(c, data.CapsuleId)
|
if c.GetSpace().IsTime {
|
||||||
|
if data.CapsuleId < 300009 {
|
||||||
|
go c.FightC.UseSkill(c, 0)
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go c.FightC.Capture(c, data.CapsuleId)
|
||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadPercent 加载进度
|
// LoadPercent 加载进度
|
||||||
func (h Controller) LoadPercent(data *fight.LoadPercentInboundInfo, c *player.Player) (result *info.LoadPercentOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) LoadPercent(data *LoadPercentInboundInfo, c *player.Player) (result *info.LoadPercentOutboundInfo, err errorcode.ErrorCode) {
|
||||||
if c.FightC == nil {
|
if c.FightC == nil {
|
||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
defer c.FightC.LoadPercent(c, int32(data.Percent))
|
go c.FightC.LoadPercent(c, int32(data.Percent))
|
||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// UsePetItemInboundInfo 使用宠物道具
|
// UsePetItemInboundInfo 使用宠物道具
|
||||||
func (h Controller) UsePetItemInboundInfo(data *fight.UsePetItemInboundInfo, c *player.Player) (result *info.UsePetIteminfo, err errorcode.ErrorCode) {
|
func (h Controller) UsePetItemInboundInfo(data *UsePetItemInboundInfo, c *player.Player) (result *info.UsePetIteminfo, err errorcode.ErrorCode) {
|
||||||
if err := h.checkFightStatus(c); err != 0 {
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer c.FightC.UseItem(c, data.CatchTime, data.ItemId)
|
if c.GetSpace().IsTime {
|
||||||
|
if data.ItemId < 300009 {
|
||||||
|
go c.FightC.UseSkill(c, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.dispatchFightActionEnvelope(c, buildLegacyUseItemEnvelope(data))
|
||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// FightChat 战斗聊天
|
// FightChat 战斗聊天
|
||||||
func (h Controller) FightChat(data *fight.ChatInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) FightChat(data *ChatInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
if err := h.checkFightStatus(c); err != 0 {
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer c.FightC.Chat(c, data.Message)
|
h.dispatchFightActionEnvelope(c, buildChatEnvelope(data))
|
||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"blazing/common/data"
|
|
||||||
"blazing/common/data/xmlres"
|
|
||||||
"blazing/common/socket/errorcode"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"blazing/logic/service/fight"
|
|
||||||
"blazing/logic/service/fight/info"
|
|
||||||
|
|
||||||
"blazing/logic/service/player"
|
|
||||||
"blazing/modules/config/service"
|
|
||||||
"blazing/modules/player/model"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/util/gconv"
|
|
||||||
"github.com/gogf/gf/v2/util/grand"
|
|
||||||
"github.com/samber/lo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// processMonID 处理怪物ID字符串,如果是多个ID则随机选择一个
|
|
||||||
func processMonID(bm string) string {
|
|
||||||
// 按空格分割字符串
|
|
||||||
monid := strings.Split(bm, " ")
|
|
||||||
|
|
||||||
// 过滤分割后可能的空字符串(如连续空格导致的空元素)
|
|
||||||
filtered := make([]string, 0, len(monid))
|
|
||||||
for _, m := range monid {
|
|
||||||
if m != "" {
|
|
||||||
filtered = append(filtered, m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
monid = filtered
|
|
||||||
|
|
||||||
var selected string
|
|
||||||
switch len(monid) {
|
|
||||||
case 0:
|
|
||||||
// 无元素时,可返回空或默认值(根据业务需求调整)
|
|
||||||
selected = ""
|
|
||||||
case 1:
|
|
||||||
// 长度为1时,取第一个(唯一的元素)
|
|
||||||
selected = monid[0]
|
|
||||||
default:
|
|
||||||
// 长度大于1时,随机选取一个
|
|
||||||
randomIdx := grand.Intn(len(monid))
|
|
||||||
selected = monid[randomIdx]
|
|
||||||
}
|
|
||||||
return selected
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlayerFightBoss 挑战地图boss
|
|
||||||
// data: 包含挑战Boss信息的输入数据
|
|
||||||
// player: 当前玩家对象
|
|
||||||
// 返回: 战斗结果和错误码
|
|
||||||
func (Controller) PlayerFightBoss(data *fight.ChallengeBossInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
|
||||||
if !p.CanFight() {
|
|
||||||
return nil, errorcode.ErrorCodes.ErrPokemonNoStamina
|
|
||||||
}
|
|
||||||
var monster *model.PetInfo
|
|
||||||
monsterInfo := &model.PlayerInfo{}
|
|
||||||
|
|
||||||
var taskID int
|
|
||||||
var canCapture int
|
|
||||||
mdata, ok := xmlres.MonsterMap[int(p.Info.MapID)]
|
|
||||||
if !ok {
|
|
||||||
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
|
||||||
}
|
|
||||||
if len(mdata.Bosses) == 0 {
|
|
||||||
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
|
||||||
}
|
|
||||||
for _, bc := range mdata.Bosses {
|
|
||||||
|
|
||||||
if bc.Id == nil {
|
|
||||||
|
|
||||||
bc.Id = gconv.PtrInt(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bc.Id == nil && data.BossId == 0) || uint32(*bc.Id) == data.BossId { //打默认第一个boss
|
|
||||||
if bc.TaskID != nil {
|
|
||||||
taskID = *bc.TaskID
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, bm := range bc.BossMon {
|
|
||||||
|
|
||||||
monster = model.GenPetInfo(
|
|
||||||
gconv.Int(processMonID(bm.MonID)), 24, //24个体
|
|
||||||
-1,
|
|
||||||
0, //野怪没特性
|
|
||||||
|
|
||||||
bm.Lv, nil)
|
|
||||||
monster.CatchTime = uint32(i)
|
|
||||||
if bm.Hp != 0 {
|
|
||||||
monster.Hp = uint32(bm.Hp)
|
|
||||||
monster.MaxHp = uint32(bm.Hp)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range strings.Split(bm.NewSeIdxs, " ") {
|
|
||||||
idx := gconv.Uint16(v)
|
|
||||||
|
|
||||||
if idx == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
EID, args := service.NewEffectService().Args(uint32(idx))
|
|
||||||
monster.EffectInfo = append(monster.EffectInfo, model.PetEffectInfo{
|
|
||||||
Idx: idx,
|
|
||||||
EID: gconv.Uint16(EID),
|
|
||||||
Args: gconv.Ints(args),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
monsterInfo.PetList = append(monsterInfo.PetList, *monster)
|
|
||||||
}
|
|
||||||
if bc.BossCatchable == 1 {
|
|
||||||
canCapture = xmlres.PetMAP[int(monster.ID)].CatchRate
|
|
||||||
|
|
||||||
if grand.Meet(1, 100) {
|
|
||||||
r := monsterInfo.PetList[0]
|
|
||||||
r.RandShiny()
|
|
||||||
monsterInfo.PetList[0] = r
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
monsterInfo.Nick = bc.Name //xmlres.PetMAP[int(monster.ID)].DefName
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if len(monsterInfo.PetList) == 0 {
|
|
||||||
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
|
||||||
}
|
|
||||||
p.Fightinfo.Status = info.BattleMode.FIGHT_WITH_NPC
|
|
||||||
p.Fightinfo.Mode = info.BattleMode.MULTI_MODE
|
|
||||||
|
|
||||||
ai := player.NewAI_player(monsterInfo)
|
|
||||||
ai.CanCapture = canCapture
|
|
||||||
ai.Prop[0] = 2
|
|
||||||
fight.NewFight(p, ai, func(foi info.FightOverInfo) {
|
|
||||||
if taskID != 0 {
|
|
||||||
if foi.Reason == 0 && foi.WinnerId == p.Info.UserID {
|
|
||||||
p.SptCompletedTask(taskID, 1)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//p.Done.Exec(model.MilestoneMode.BOSS, []uint32{p.Info.MapID, data.BossId, uint32(foi.Reason)}, nil)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil, -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnPlayerFightNpcMonster 战斗野怪
|
|
||||||
// data: 包含战斗野怪信息的输入数据
|
|
||||||
// player: 当前玩家对象
|
|
||||||
// 返回: 战斗结果和错误码
|
|
||||||
func (Controller) OnPlayerFightNpcMonster(data1 *fight.FightNpcMonsterInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
|
||||||
if !p.CanFight() {
|
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
|
||||||
}
|
|
||||||
refPet := p.OgreInfo.Data[data1.Number]
|
|
||||||
if refPet.Id == 0 {
|
|
||||||
|
|
||||||
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
|
||||||
}
|
|
||||||
if refPet.Ext != 0 {
|
|
||||||
refPet.Id = refPet.Ext
|
|
||||||
|
|
||||||
}
|
|
||||||
monster := model.GenPetInfo(
|
|
||||||
int(refPet.Id), -1,
|
|
||||||
-1,
|
|
||||||
0, //野怪没特性
|
|
||||||
|
|
||||||
int(refPet.Lv),
|
|
||||||
refPet.ShinyInfo)
|
|
||||||
if refPet.Ext != 0 {
|
|
||||||
if grand.Meet(3, 100) {
|
|
||||||
monster.RandShiny()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
monsterInfo := &model.PlayerInfo{}
|
|
||||||
monsterInfo.Nick = xmlres.PetMAP[int(monster.ID)].DefName
|
|
||||||
monsterInfo.PetList = append(monsterInfo.PetList, *monster)
|
|
||||||
ai := player.NewAI_player(monsterInfo)
|
|
||||||
ai.CanCapture = handleNPCFightSpecial(monster.ID)
|
|
||||||
|
|
||||||
p.Fightinfo.Status = info.BattleMode.FIGHT_WITH_NPC //打野怪
|
|
||||||
p.Fightinfo.Mode = info.BattleMode.MULTI_MODE //多人模式
|
|
||||||
|
|
||||||
fight.NewFight(p, ai, func(foi info.FightOverInfo) {
|
|
||||||
//p.Done.Exec(model.MilestoneMode.Moster, []uint32{p.Info.MapID, monsterInfo.PetList[0].ID, uint32(foi.Reason)}, nil)
|
|
||||||
if foi.Reason == 0 && foi.WinnerId == p.Info.UserID {
|
|
||||||
|
|
||||||
if !p.CanGetExp() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
exp := uint32(xmlres.PetMAP[int(monster.ID)].YieldingExp) * monster.Level / 7
|
|
||||||
items := &info.S2C_GET_BOSS_MONSTER{
|
|
||||||
//EV: 45,
|
|
||||||
EXP: exp * 2,
|
|
||||||
}
|
|
||||||
if refPet.Item != 0 {
|
|
||||||
count := uint32(grand.Intn(2) + 1)
|
|
||||||
p.ItemAdd(refPet.Item, count)
|
|
||||||
items.ItemList = append(items.ItemList, data.ItemInfo{
|
|
||||||
ItemId: refPet.Item,
|
|
||||||
ItemCnt: count,
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
evs := gconv.Uint32s(strings.Split(xmlres.PetMAP[int(monster.ID)].YieldingEV, " "))
|
|
||||||
items.EV = lo.Sum(evs) - 1
|
|
||||||
p.Info.EVPool += lo.Sum(evs) //给予累计学习力
|
|
||||||
foi.Winpet.AddEV(evs)
|
|
||||||
|
|
||||||
p.Info.ExpPool += exp * 4
|
|
||||||
p.AddPetExp(foi.Winpet, uint32(exp)*2)
|
|
||||||
p.SendPackCmd(8004, items)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil, -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleNPCFightSpecial 处理NPC战斗特殊情况
|
|
||||||
func handleNPCFightSpecial(petID uint32) int {
|
|
||||||
|
|
||||||
npcPetID := int(petID)
|
|
||||||
petCfg, ok := xmlres.PetMAP[npcPetID]
|
|
||||||
if !ok {
|
|
||||||
// log.Error(context.Background(), "NPC宠物配置不存在", "petID", npcPetID)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
catchRate := gconv.Int(petCfg.CatchRate)
|
|
||||||
return catchRate
|
|
||||||
}
|
|
||||||
314
logic/controller/fight_boss野怪和地图怪.go
Normal file
314
logic/controller/fight_boss野怪和地图怪.go
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/data"
|
||||||
|
"blazing/common/data/xmlres"
|
||||||
|
"blazing/common/socket/errorcode"
|
||||||
|
|
||||||
|
"blazing/logic/service/fight"
|
||||||
|
fightinfo "blazing/logic/service/fight/info"
|
||||||
|
"blazing/logic/service/fight/input"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
configmodel "blazing/modules/config/model"
|
||||||
|
"blazing/modules/config/service"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rewardItemExpPool = 3
|
||||||
|
groupBossSlotLimit = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlayerFightBoss 挑战地图boss
|
||||||
|
func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err = p.CanFight(); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mapNode := p.GetSpace().GetMatchedMapNode(req.BossId)
|
||||||
|
if mapNode == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
bossConfigs, err := loadMapBossConfigs(mapNode)
|
||||||
|
if err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
monsterInfo, leadMonsterID, err := buildBossMonsterInfo(mapNode.NodeName, bossConfigs)
|
||||||
|
if err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
|
||||||
|
p.Fightinfo.Mode = resolveMapNodeFightMode(mapNode)
|
||||||
|
|
||||||
|
ai := player.NewAI_player(monsterInfo)
|
||||||
|
ai.CanCapture = resolveBossCaptureRate(bossConfigs[0].IsCapture, leadMonsterID)
|
||||||
|
ai.BossScript = bossConfigs[0].Script
|
||||||
|
ai.AddBattleProp(0, 2)
|
||||||
|
|
||||||
|
var fightC *fight.FightC
|
||||||
|
fightC, err = startMapBossFight(mapNode, p, ai, func(foi model.FightOverInfo) {
|
||||||
|
if mapNode.WinBonusID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if shouldGrantBossWinBonus(fightC, p.Info.UserID, bossConfigs[0], foi) {
|
||||||
|
p.SptCompletedTask(mapNode.WinBonusID, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMapBossFight(
|
||||||
|
mapNode *configmodel.MapNode,
|
||||||
|
p *player.Player,
|
||||||
|
ai *player.AI_player,
|
||||||
|
fn func(model.FightOverInfo),
|
||||||
|
) (*fight.FightC, errorcode.ErrorCode) {
|
||||||
|
ourPets := p.GetPetInfo(p.CurrentMapPetLevelLimit())
|
||||||
|
oppPets := ai.GetPetInfo(0)
|
||||||
|
if mapNode != nil && mapNode.IsGroupBoss != 0 {
|
||||||
|
if len(ourPets) > 0 && len(oppPets) > 0 {
|
||||||
|
slotLimit := groupBossSlotLimit
|
||||||
|
if mapNode.PkFlag != 0 {
|
||||||
|
slotLimit = 1
|
||||||
|
}
|
||||||
|
return fight.NewLegacyGroupFightSingleController(p, ai, ourPets, oppPets, slotLimit, fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fight.NewFight(p, ai, ourPets, oppPets, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveMapNodeFightMode(mapNode *configmodel.MapNode) uint32 {
|
||||||
|
if mapNode != nil && mapNode.PkFlag != 0 {
|
||||||
|
return fightinfo.BattleMode.SINGLE_MODE
|
||||||
|
}
|
||||||
|
return fightinfo.BattleMode.MULTI_MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPlayerFightNpcMonster 战斗野怪
|
||||||
|
func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err = p.CanFight(); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if int(req.Number) >= len(p.Data) {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotHere
|
||||||
|
}
|
||||||
|
|
||||||
|
refPet := p.Data[req.Number]
|
||||||
|
monster, monsterInfo, err := buildNpcMonsterInfo(refPet, p.Info.MapID)
|
||||||
|
if err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ai := player.NewAI_player(monsterInfo)
|
||||||
|
ai.CanCapture = refPet.IsCapture
|
||||||
|
|
||||||
|
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
|
||||||
|
p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
|
||||||
|
|
||||||
|
_, err = fight.NewFight(p, ai, p.GetPetInfo(p.CurrentMapPetLevelLimit()), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
|
||||||
|
handleNpcFightRewards(p, foi, monster)
|
||||||
|
})
|
||||||
|
if err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMapBossConfigs(mapNode *configmodel.MapNode) ([]configmodel.BossConfig, errorcode.ErrorCode) {
|
||||||
|
if mapNode == nil || len(mapNode.BossIds) == 0 {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
bossID := mapNode.BossIds[0]
|
||||||
|
if len(mapNode.BossIds) > 1 {
|
||||||
|
bossID = mapNode.BossIds[grand.Intn(len(mapNode.BossIds))]
|
||||||
|
}
|
||||||
|
|
||||||
|
bossConfigs := service.NewBossService().Get(bossID)
|
||||||
|
if len(bossConfigs) == 0 {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return bossConfigs, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBossMonsterInfo(nodeName string, bossConfigs []configmodel.BossConfig) (*model.PlayerInfo, uint32, errorcode.ErrorCode) {
|
||||||
|
monsterInfo := &model.PlayerInfo{Nick: nodeName}
|
||||||
|
var leadMonsterID uint32
|
||||||
|
|
||||||
|
for i, bossConfig := range bossConfigs {
|
||||||
|
dv, generation := bossFightPetArgs(bossConfig.IsCapture)
|
||||||
|
monster := model.GenPetInfo(
|
||||||
|
gconv.Int(bossConfig.MonID),
|
||||||
|
dv,
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
int(bossConfig.Lv),
|
||||||
|
nil,
|
||||||
|
generation,
|
||||||
|
)
|
||||||
|
if monster == nil {
|
||||||
|
return nil, 0, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
monster.CatchTime = uint32(i)
|
||||||
|
monster.ConfigBoss(bossConfig.PetBaseConfig)
|
||||||
|
appendPetEffects(monster, bossConfig.Effect)
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
leadMonsterID = monster.ID
|
||||||
|
}
|
||||||
|
monsterInfo.PetList = append(monsterInfo.PetList, *monster)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(monsterInfo.PetList) == 0 {
|
||||||
|
return nil, 0, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
if bossConfigs[0].IsCapture == 1 {
|
||||||
|
monsterInfo.PetList[0].ShinyInfo = make([]data.GlowFilter, 0)
|
||||||
|
if grand.Meet(1, 500) {
|
||||||
|
monsterInfo.PetList[0].RandomByWeightShiny()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return monsterInfo, leadMonsterID, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func bossFightPetArgs(canCapture int) (dv int, generation int) {
|
||||||
|
if canCapture == 1 {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
return 24, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendPetEffects(monster *model.PetInfo, effectIDs []uint32) {
|
||||||
|
effects := service.NewEffectService().Args(effectIDs)
|
||||||
|
for _, effect := range effects {
|
||||||
|
monster.EffectInfo = append(monster.EffectInfo, model.PetEffectInfo{
|
||||||
|
Idx: uint16(effect.ID),
|
||||||
|
EID: gconv.Uint16(effect.Eid),
|
||||||
|
Args: gconv.Ints(effect.Args),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveBossCaptureRate(canCapture int, petID uint32) int {
|
||||||
|
if canCapture == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
petCfg, ok := xmlres.PetMAP[int(petID)]
|
||||||
|
if !ok || petCfg.CatchRate == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return petCfg.CatchRate
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldGrantBossWinBonus(fightC *fight.FightC, playerID uint32, bossConfig configmodel.BossConfig, foi model.FightOverInfo) bool {
|
||||||
|
if len(bossConfig.Rule) == 0 {
|
||||||
|
return foi.Reason == 0 && foi.WinnerId == playerID
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ruleConfig := range service.NewFightRuleService().GetByRuleIdxs(bossConfig.Rule) {
|
||||||
|
rule := input.GetRule(int64(ruleConfig.RuleIdx))
|
||||||
|
if rule == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rule.SetArgs(ruleConfig.Args...)
|
||||||
|
if !rule.Exec(fightC, &foi) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInfo, *model.PlayerInfo, errorcode.ErrorCode) {
|
||||||
|
if refPet.ID == 0 {
|
||||||
|
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotHere
|
||||||
|
}
|
||||||
|
|
||||||
|
monster := model.GenPetInfo(
|
||||||
|
refPet.GetID(),
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
refPet.GetLevel(),
|
||||||
|
refPet.ShinyInfo,
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
if monster == nil {
|
||||||
|
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
monster.CatchMap = mapID
|
||||||
|
if refPet.Ext != 0 && grand.Meet(1, 500) {
|
||||||
|
monster.RandomByWeightShiny()
|
||||||
|
}
|
||||||
|
|
||||||
|
petCfg, ok := xmlres.PetMAP[int(monster.ID)]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
monsterInfo := &model.PlayerInfo{
|
||||||
|
Nick: petCfg.DefName,
|
||||||
|
PetList: []model.PetInfo{*monster},
|
||||||
|
}
|
||||||
|
return monster, monsterInfo, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleNpcFightRewards(p *player.Player, foi model.FightOverInfo, monster *model.PetInfo) {
|
||||||
|
if foi.Reason != 0 || foi.WinnerId != p.Info.UserID || !p.CanGet() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
petCfg, ok := xmlres.PetMAP[int(monster.ID)]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exp := uint32(petCfg.YieldingExp) * monster.Level / 7
|
||||||
|
addlevel, poolevel := p.CanGetExp()
|
||||||
|
addexp := gconv.Float32(addlevel * gconv.Float32(exp))
|
||||||
|
poolexp := gconv.Float32(poolevel) * gconv.Float32(exp)
|
||||||
|
rewards := &fightinfo.S2C_GET_BOSS_MONSTER{}
|
||||||
|
|
||||||
|
p.ItemAdd(3, int64(poolexp+addexp))
|
||||||
|
rewards.AddItem(rewardItemExpPool, uint32(poolexp))
|
||||||
|
p.AddPetExp(foi.Winpet, int64(addexp))
|
||||||
|
|
||||||
|
if p.CanGetItem() {
|
||||||
|
itemID := p.GetSpace().GetDrop()
|
||||||
|
if itemID != 0 {
|
||||||
|
count := uint32(grand.N(1, 2))
|
||||||
|
if p.ItemAdd(itemID, int64(count)) {
|
||||||
|
rewards.AddItem(uint32(itemID), count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
petType := int64(petCfg.Type)
|
||||||
|
if monster.IsShiny() && p.CanGetXUAN() && petType < 16 {
|
||||||
|
xuanID := uint32(400686 + petType)
|
||||||
|
count := uint32(grand.N(1, 2))
|
||||||
|
if p.ItemAdd(int64(xuanID), int64(count)) {
|
||||||
|
rewards.AddItem(xuanID, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rewards.HasReward() {
|
||||||
|
p.SendPackCmd(8004, rewards)
|
||||||
|
}
|
||||||
|
foi.Winpet.AddEV(petCfg.YieldingEVValues)
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/modules/config/service"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
|
||||||
"blazing/logic/service/common"
|
"blazing/logic/service/common"
|
||||||
"blazing/logic/service/fight"
|
"blazing/logic/service/fight"
|
||||||
@@ -11,22 +13,56 @@ import (
|
|||||||
|
|
||||||
//大乱斗
|
//大乱斗
|
||||||
|
|
||||||
func (h Controller) PetMelee(data *fight.StartPetWarInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) PetMelee(data *StartPetWarInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
|
||||||
c.Fightinfo.Mode = info.BattleMode.PET_MELEE
|
c.Fightinfo.Mode = info.BattleMode.PET_MELEE
|
||||||
c.Fightinfo.Status = info.BattleMode.PET_MELEE
|
c.Fightinfo.Status = info.BattleMode.PET_MELEE
|
||||||
|
var mepet []model.PetInfo
|
||||||
|
|
||||||
|
for i, v := range service.NewMELEEService().Def() {
|
||||||
|
|
||||||
|
if v.Lv == 0 {
|
||||||
|
v.Lv = 100
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pet := model.GenPetInfo(int(v.MonID), 24, int(v.Nature), int(v.Effect[0]), int(v.Lv), nil, 0)
|
||||||
|
|
||||||
|
pet.ConfigBoss(v)
|
||||||
|
pet.CatchTime = c.GetInfo().UserID + uint32(i)*1000000
|
||||||
|
pet.Cure()
|
||||||
|
mepet = append(mepet, *pet)
|
||||||
|
|
||||||
|
}
|
||||||
|
if len(mepet) < 6 {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
}
|
||||||
err = c.JoinFight(func(p common.PlayerI) bool {
|
err = c.JoinFight(func(p common.PlayerI) bool {
|
||||||
_, err = fight.NewFight(p, c, func(foi info.FightOverInfo) {
|
_, err = fight.NewFight(p, c, mepet[:3], mepet[3:], func(foi model.FightOverInfo) {
|
||||||
if foi.Reason == 0 { //我放获胜
|
if foi.Reason == 0 { //我放获胜
|
||||||
|
|
||||||
if foi.WinnerId == c.GetInfo().UserID {
|
if foi.WinnerId == c.GetInfo().UserID {
|
||||||
c.Info.MessWin += 1
|
c.Info.MessWin += 1
|
||||||
|
c.MessWin(true)
|
||||||
|
p.MessWin(false)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
p.GetInfo().MessWin += 1
|
p.GetInfo().MessWin += 1
|
||||||
|
p.MessWin(true)
|
||||||
|
c.MessWin(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
if foi.Reason == model.BattleOverReason.PlayerOffline {
|
||||||
|
if foi.WinnerId == c.GetInfo().UserID {
|
||||||
|
|
||||||
|
p.MessWin(false)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
c.MessWin(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}) ///开始对战,房主方以及被邀请方
|
}) ///开始对战,房主方以及被邀请方
|
||||||
return err <= 0
|
return err <= 0
|
||||||
@@ -34,23 +70,48 @@ func (h Controller) PetMelee(data *fight.StartPetWarInboundInfo, c *player.Playe
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
func (h Controller) PetKing(data *fight.PetKingJoinInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
|
||||||
|
// PetKing 处理控制器请求。
|
||||||
|
func (h Controller) PetKing(data *PetKingJoinInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
|
||||||
c.Fightinfo.Status = info.BattleMode.PET_TOPLEVEL
|
c.Fightinfo.Status = info.BattleMode.PET_TOPLEVEL
|
||||||
|
// ElementTypeNumbers 是控制器层共享变量。
|
||||||
|
var ElementTypeNumbers = []int{1, 2, 3, 5, 11, 4, 6, 7, 9}
|
||||||
|
|
||||||
switch data.Type {
|
switch data.Type {
|
||||||
case 5:
|
case 5:
|
||||||
c.Fightinfo.Mode = info.BattleMode.SINGLE_MODE
|
c.Fightinfo.Mode = info.BattleMode.SINGLE_MODE
|
||||||
case 6:
|
case 6:
|
||||||
c.Fightinfo.Mode = info.BattleMode.MULTI_MODE
|
c.Fightinfo.Mode = info.BattleMode.MULTI_MODE
|
||||||
|
case 11:
|
||||||
|
//草","水","火","电","战斗","飞行","机械","地面","冰"
|
||||||
|
// 按顺序:草、水、火、电、战斗、飞行、机械、地面、冰
|
||||||
|
|
||||||
|
//println("11", c.GetPetInfo()[0].Type(), ElementTypeNumbers[data.FightType-1])
|
||||||
|
if c.GetPetInfo(0)[0].Type() != int(ElementTypeNumbers[data.FightType-1]) {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrVictoryConditionNotMet)
|
||||||
|
}
|
||||||
|
c.Fightinfo.Mode = info.BattleMode.SINGLE_MODE
|
||||||
|
c.Fightinfo.FightType = data.FightType
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.JoinFight(func(p common.PlayerI) bool {
|
err = c.JoinFight(func(p common.PlayerI) bool {
|
||||||
_, err = fight.NewFight(p, c, func(foi info.FightOverInfo) {
|
|
||||||
|
_, err = fight.NewFight(p, c, p.GetInfo().PetList, c.GetInfo().PetList, func(foi model.FightOverInfo) {
|
||||||
if foi.Reason == 0 { //我放获胜
|
if foi.Reason == 0 { //我放获胜
|
||||||
if foi.WinnerId == c.GetInfo().UserID {
|
switch data.Type {
|
||||||
c.Info.MonKingWin += 1
|
case 11:
|
||||||
} else {
|
if foi.WinnerId == c.GetInfo().UserID {
|
||||||
p.GetInfo().MonKingWin += 1
|
c.ItemAdd(80000000+int64(ElementTypeNumbers[data.FightType-1]), 1)
|
||||||
|
} else {
|
||||||
|
p.ItemAdd(80000000+int64(ElementTypeNumbers[data.FightType-1]), 1)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if foi.WinnerId == c.GetInfo().UserID {
|
||||||
|
c.Info.MonKingWin += 1
|
||||||
|
} else {
|
||||||
|
p.GetInfo().MonKingWin += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/modules/player/model"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"blazing/logic/service/common"
|
"blazing/logic/service/common"
|
||||||
@@ -11,8 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 接收战斗或者取消战斗的包
|
// 接收战斗或者取消战斗的包
|
||||||
func (h Controller) OnPlayerHandleFightInvite(data *fight.HandleFightInviteInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) OnPlayerHandleFightInvite(data *HandleFightInviteInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
if c.GetSpace().Owner.UserID == c.Info.UserID {
|
if c.IsArenaPVPLocked() {
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
}
|
}
|
||||||
if c.GetSpace().Owner.UserID == data.UserID {
|
if c.GetSpace().Owner.UserID == data.UserID {
|
||||||
@@ -23,8 +24,9 @@ func (h Controller) OnPlayerHandleFightInvite(data *fight.HandleFightInviteInbou
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.CanFight() {
|
r := c.CanFight()
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
if c.CanFight() != 0 {
|
||||||
|
return nil, r
|
||||||
}
|
}
|
||||||
|
|
||||||
//c.Fightinfo.Status = info.BattleMode.FIGHT_WITH_NPC
|
//c.Fightinfo.Status = info.BattleMode.FIGHT_WITH_NPC
|
||||||
@@ -53,7 +55,9 @@ func (h Controller) OnPlayerHandleFightInvite(data *fight.HandleFightInviteInbou
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = fight.NewFight(v, c, func(foi info.FightOverInfo) {
|
_, err = fight.NewFight(v, c, v.GetPetInfo(100), c.GetPetInfo(100), func(foi model.FightOverInfo) {
|
||||||
|
|
||||||
|
//println("好友对战测试", foi.Reason)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -74,8 +78,8 @@ func (h Controller) OnPlayerHandleFightInvite(data *fight.HandleFightInviteInbou
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 邀请其他人进行战斗
|
// 邀请其他人进行战斗
|
||||||
func (h Controller) OnPlayerInviteOtherFight(data *fight.InviteToFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) OnPlayerInviteOtherFight(data *InviteToFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
if c.GetSpace().Owner.UserID == c.Info.UserID {
|
if c.IsArenaPVPLocked() {
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
}
|
}
|
||||||
if c.GetSpace().Owner.ChallengerID == c.Info.UserID {
|
if c.GetSpace().Owner.ChallengerID == c.Info.UserID {
|
||||||
@@ -99,7 +103,7 @@ func (h Controller) OnPlayerInviteOtherFight(data *fight.InviteToFightInboundInf
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 取消队列
|
// 取消队列
|
||||||
func (h Controller) OnPlayerCanceledOtherInviteFight(data *fight.InviteFightCancelInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) OnPlayerCanceledOtherInviteFight(data *InviteFightCancelInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
atomic.StoreUint32(&c.Fightinfo.Mode, 0) //设置状态为0
|
atomic.StoreUint32(&c.Fightinfo.Mode, 0) //设置状态为0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"blazing/common/socket/errorcode"
|
|
||||||
"blazing/common/utils"
|
|
||||||
"blazing/logic/service/fight"
|
|
||||||
fightinfo "blazing/logic/service/fight/info"
|
|
||||||
"blazing/logic/service/player"
|
|
||||||
"blazing/logic/service/space/info"
|
|
||||||
configmodel "blazing/modules/config/model"
|
|
||||||
"blazing/modules/config/service"
|
|
||||||
"blazing/modules/player/model"
|
|
||||||
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/util/gconv"
|
|
||||||
"github.com/jinzhu/copier"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h Controller) FreshOPEN(data *fight.C2S_OPEN_DARKPORTAL, c *player.Player) (result *fight.S2C_OPEN_DARKPORTAL, err errorcode.ErrorCode) {
|
|
||||||
|
|
||||||
result = &fight.S2C_OPEN_DARKPORTAL{}
|
|
||||||
c.Info.CurrentFreshStage = utils.Max(c.Info.CurrentFreshStage, 1)
|
|
||||||
c.Info.CurrentStage = utils.Max(c.Info.CurrentStage, 1)
|
|
||||||
boss := service.NewTower110Service().Boss(uint32(data.Level))
|
|
||||||
result = &fight.S2C_OPEN_DARKPORTAL{}
|
|
||||||
for _, v := range boss.BossIds {
|
|
||||||
r := service.NewBossService().Get(v)
|
|
||||||
result.CurBossID = uint32(r.MonID)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
c.CurDark = uint32(data.Level)
|
|
||||||
defer c.GetSpace().LeaveMap(c)
|
|
||||||
return result, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// FreshChoiceFightLevel 处理玩家选择挑战模式(试炼之塔或勇者之塔)
|
|
||||||
// 根据不同的CMD值设置玩家的挑战状态和地图信息,并返回当前挑战层级信息
|
|
||||||
// 参数:
|
|
||||||
//
|
|
||||||
// data: 客户端发送的挑战层级选择请求数据,包含CMD和挑战层级
|
|
||||||
// c: 玩家对象,包含玩家的详细信息
|
|
||||||
//
|
|
||||||
// 返回值:
|
|
||||||
//
|
|
||||||
// result: 服务器返回给客户端的挑战层级信息,包含当前战斗层级和Boss ID
|
|
||||||
// err: 错误码,表示处理过程中是否出现错误
|
|
||||||
func (h Controller) FreshChoiceFightLevel(data *fight.C2S_FRESH_CHOICE_FIGHT_LEVEL, c *player.Player) (result *fight.S2C_FreshChoiceLevelRequestInfo, err errorcode.ErrorCode) {
|
|
||||||
|
|
||||||
result = &fight.S2C_FreshChoiceLevelRequestInfo{}
|
|
||||||
c.Info.CurrentFreshStage = utils.Max(c.Info.CurrentFreshStage, 1)
|
|
||||||
c.Info.CurrentStage = utils.Max(c.Info.CurrentStage, 1)
|
|
||||||
|
|
||||||
if data.Level > 0 {
|
|
||||||
switch data.Head.CMD {
|
|
||||||
case 2428: //试炼之塔
|
|
||||||
|
|
||||||
c.Info.CurrentFreshStage = uint32((data.Level-1)*10) + 1
|
|
||||||
case 2414: //勇者之塔
|
|
||||||
|
|
||||||
c.Info.CurrentStage = uint32((data.Level-1)*10) + 1
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
var boss *configmodel.BaseTowerConfig
|
|
||||||
switch data.Head.CMD {
|
|
||||||
case 2428: //试炼之塔
|
|
||||||
|
|
||||||
result.CurFightLevel = uint32(c.Info.CurrentFreshStage)
|
|
||||||
boss = service.NewTower600Service().Boss(c.Info.CurrentFreshStage)
|
|
||||||
|
|
||||||
case 2414: //勇者之塔
|
|
||||||
|
|
||||||
result.CurFightLevel = uint32(c.Info.CurrentStage)
|
|
||||||
boss = service.NewTower500Service().Boss(c.Info.CurrentStage)
|
|
||||||
//next := service.NewTower600Service().Boss(c.Info.CurrentFreshStage + 1)
|
|
||||||
|
|
||||||
}
|
|
||||||
if boss != nil {
|
|
||||||
|
|
||||||
for _, v := range boss.BossIds {
|
|
||||||
r := service.NewBossService().Get(v)
|
|
||||||
result.BossId = append(result.BossId, uint32(r.MonID))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
// 重置玩家的Canmon标志位为0,表示可以刷怪
|
|
||||||
atomic.StoreUint32(&c.Canmon, 0)
|
|
||||||
// 在函数结束时将玩家传送到对应地图
|
|
||||||
defer c.GetSpace().LeaveMap(c)
|
|
||||||
return result, 0
|
|
||||||
}
|
|
||||||
func (h Controller) FreshLeaveFightLevel(data *fight.FRESH_LEAVE_FIGHT_LEVEL, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
|
||||||
|
|
||||||
defer c.GetSpace().EnterMap(c)
|
|
||||||
|
|
||||||
out := info.NewOutInfo()
|
|
||||||
copier.CopyWithOption(out, c.GetInfo(), copier.Option{DeepCopy: true})
|
|
||||||
|
|
||||||
c.SendPackCmd(2001, out)
|
|
||||||
return result, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h Controller) PetTawor(data *fight.StartTwarInboundInfo, c *player.Player) (result *fight.S2C_ChoiceLevelRequestInfo, err errorcode.ErrorCode) {
|
|
||||||
if !c.CanFight() {
|
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
|
||||||
}
|
|
||||||
c.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
|
|
||||||
c.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
|
|
||||||
monsterInfo := &model.PlayerInfo{}
|
|
||||||
var boss *configmodel.BaseTowerConfig
|
|
||||||
var next *configmodel.BaseTowerConfig
|
|
||||||
result = &fight.S2C_ChoiceLevelRequestInfo{}
|
|
||||||
switch data.Head.CMD {
|
|
||||||
case 2429: //试炼之塔
|
|
||||||
boss = service.NewTower600Service().Boss(c.Info.CurrentFreshStage)
|
|
||||||
next = service.NewTower600Service().Boss(c.Info.CurrentFreshStage + 1)
|
|
||||||
|
|
||||||
result.CurFightLevel = uint32(c.Info.CurrentFreshStage)
|
|
||||||
case 2415: //勇者之塔
|
|
||||||
boss = service.NewTower500Service().Boss(c.Info.CurrentStage)
|
|
||||||
next = service.NewTower500Service().Boss(c.Info.CurrentStage + 1)
|
|
||||||
|
|
||||||
result.CurFightLevel = uint32(c.Info.CurrentStage)
|
|
||||||
case 2425:
|
|
||||||
boss = service.NewTower110Service().Boss(c.CurDark)
|
|
||||||
}
|
|
||||||
if next != nil {
|
|
||||||
for _, v := range next.BossIds {
|
|
||||||
r := service.NewBossService().Get(v)
|
|
||||||
result.BossID = append(result.BossID, uint32(r.MonID))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, v := range boss.BossIds {
|
|
||||||
r := service.NewBossService().Get(v)
|
|
||||||
if r != nil {
|
|
||||||
|
|
||||||
monster := model.GenPetInfo(int(r.MonID), 24, int(r.Nature), 0, int(r.Lv), nil)
|
|
||||||
if r.Hp != 0 {
|
|
||||||
monster.Hp = uint32(r.Hp)
|
|
||||||
monster.MaxHp = uint32(r.Hp)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, v := range r.Prop {
|
|
||||||
if v != 0 {
|
|
||||||
monster.Prop[i] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.SKill) != 0 {
|
|
||||||
for i := 0; i < len(monster.SkillList); i++ {
|
|
||||||
if r.SKill[i] != 0 {
|
|
||||||
monster.SkillList[i].ID = r.SKill[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Effect) != 0 {
|
|
||||||
|
|
||||||
for _, v := range r.Effect {
|
|
||||||
|
|
||||||
EID, args := service.NewEffectService().Args(v)
|
|
||||||
monster.EffectInfo = append(monster.EffectInfo, model.PetEffectInfo{
|
|
||||||
Idx: uint16(v),
|
|
||||||
EID: gconv.Uint16(EID),
|
|
||||||
Args: gconv.Ints(args),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
monster.CatchTime = uint32(i)
|
|
||||||
monsterInfo.PetList = append(monsterInfo.PetList, *monster)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ai := player.NewAI_player(monsterInfo)
|
|
||||||
_, err = fight.NewFight(c, ai, func(foi fightinfo.FightOverInfo) {
|
|
||||||
if foi.Reason == 0 && foi.WinnerId == c.Info.UserID { //我放获胜
|
|
||||||
switch data.Head.CMD {
|
|
||||||
case 2429: //试炼之塔
|
|
||||||
c.TawerCompletedTask(600, int(c.Info.CurrentFreshStage))
|
|
||||||
c.Info.CurrentFreshStage++
|
|
||||||
if c.Info.CurrentFreshStage >= c.Info.MaxFreshStage {
|
|
||||||
c.Info.MaxFreshStage = c.Info.CurrentFreshStage
|
|
||||||
}
|
|
||||||
|
|
||||||
case 2415: //勇者之塔
|
|
||||||
c.TawerCompletedTask(500, int(c.Info.CurrentStage))
|
|
||||||
c.Info.CurrentStage++
|
|
||||||
if c.Info.CurrentStage >= c.Info.MaxStage {
|
|
||||||
c.Info.MaxStage = c.Info.CurrentStage
|
|
||||||
}
|
|
||||||
case 2425:
|
|
||||||
c.TawerCompletedTask(110, int(c.CurDark))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}) ///开始对战,房主方以及被邀请方
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
79
logic/controller/fight_unified.go
Normal file
79
logic/controller/fight_unified.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
|
||||||
|
"blazing/logic/service/fight"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dispatchFightActionEnvelope 把控制器层收到的统一动作结构分发回现有 FightI 接口。
|
||||||
|
func (h Controller) dispatchFightActionEnvelope(c *player.Player, envelope fight.FightActionEnvelope) {
|
||||||
|
if c == nil || c.FightC == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch envelope.ActionType {
|
||||||
|
case fight.FightActionTypeSkill:
|
||||||
|
go c.FightC.UseSkillAt(c, envelope.SkillID, envelope.ActorIndex, envelope.EncodedTargetIndex())
|
||||||
|
case fight.FightActionTypeItem:
|
||||||
|
go c.FightC.UseItemAt(c, envelope.CatchTime, envelope.ItemID, envelope.ActorIndex, envelope.EncodedTargetIndex())
|
||||||
|
case fight.FightActionTypeChange:
|
||||||
|
go c.FightC.ChangePetAt(c, envelope.CatchTime, envelope.ActorIndex)
|
||||||
|
case fight.FightActionTypeEscape:
|
||||||
|
go c.FightC.Over(c, model.BattleOverReason.PlayerEscape)
|
||||||
|
case fight.FightActionTypeChat:
|
||||||
|
go c.FightC.Chat(c, envelope.Chat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildLegacyUseSkillEnvelope 把旧 2405 技能包映射成统一动作结构。
|
||||||
|
func buildLegacyUseSkillEnvelope(data *UseSkillInInfo) fight.FightActionEnvelope {
|
||||||
|
if data == nil {
|
||||||
|
return fight.NewSkillActionEnvelope(0, 0, 0, fight.SkillTargetOpponent, 0)
|
||||||
|
}
|
||||||
|
return fight.NewSkillActionEnvelope(data.SkillId, 0, 0, fight.SkillTargetOpponent, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildIndexedUseSkillEnvelope 把 7505 多战位技能包映射成统一动作结构。
|
||||||
|
func buildIndexedUseSkillEnvelope(data *UseSkillAtInboundInfo) fight.FightActionEnvelope {
|
||||||
|
if data == nil {
|
||||||
|
return fight.NewSkillActionEnvelope(0, 0, 0, fight.SkillTargetOpponent, 0)
|
||||||
|
}
|
||||||
|
return fight.NewSkillActionEnvelope(
|
||||||
|
data.SkillId,
|
||||||
|
int(data.ActorIndex),
|
||||||
|
int(data.TargetIndex),
|
||||||
|
data.TargetRelation,
|
||||||
|
data.AtkType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildLegacyUseItemEnvelope 把旧 2406 道具包映射成统一动作结构。
|
||||||
|
func buildLegacyUseItemEnvelope(data *UsePetItemInboundInfo) fight.FightActionEnvelope {
|
||||||
|
if data == nil {
|
||||||
|
return fight.NewItemActionEnvelope(0, 0, 0, 0, fight.SkillTargetOpponent)
|
||||||
|
}
|
||||||
|
return fight.NewItemActionEnvelope(data.CatchTime, data.ItemId, 0, 0, fight.SkillTargetOpponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildLegacyChangeEnvelope 把旧 2407 切宠包映射成统一动作结构。
|
||||||
|
func buildLegacyChangeEnvelope(data *ChangePetInboundInfo) fight.FightActionEnvelope {
|
||||||
|
if data == nil {
|
||||||
|
return fight.NewChangeActionEnvelope(0, 0)
|
||||||
|
}
|
||||||
|
return fight.NewChangeActionEnvelope(data.CatchTime, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildLegacyEscapeEnvelope 构造旧 2410 逃跑包对应的统一动作结构。
|
||||||
|
func buildLegacyEscapeEnvelope() fight.FightActionEnvelope {
|
||||||
|
return fight.NewEscapeActionEnvelope()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildChatEnvelope 把战斗聊天包映射成统一动作结构。
|
||||||
|
func buildChatEnvelope(data *ChatInfo) fight.FightActionEnvelope {
|
||||||
|
if data == nil {
|
||||||
|
return fight.NewChatActionEnvelope("")
|
||||||
|
}
|
||||||
|
return fight.NewChatActionEnvelope(data.Message)
|
||||||
|
}
|
||||||
258
logic/controller/fight_塔.go
Normal file
258
logic/controller/fight_塔.go
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/common/utils"
|
||||||
|
"blazing/logic/service/fight"
|
||||||
|
fightinfo "blazing/logic/service/fight/info"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
"blazing/logic/service/space/info"
|
||||||
|
configmodel "blazing/modules/config/model"
|
||||||
|
"blazing/modules/config/service"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
"github.com/jinzhu/copier"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
towerCmdChoiceTrial uint32 = 2428
|
||||||
|
towerCmdChoiceBrave uint32 = 2414
|
||||||
|
towerCmdFightTrial uint32 = 2429
|
||||||
|
towerCmdFightBrave uint32 = 2415
|
||||||
|
towerCmdFightDark uint32 = 2425
|
||||||
|
|
||||||
|
towerTaskDark int = 110
|
||||||
|
towerTaskBrave int = 500
|
||||||
|
towerTaskTrial int = 600
|
||||||
|
)
|
||||||
|
|
||||||
|
type towerChoiceState struct {
|
||||||
|
currentLevel *uint32
|
||||||
|
maxLevel *uint32
|
||||||
|
service *service.TowerService
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暗黑门进入boss
|
||||||
|
func (h Controller) FreshOpen(data *C2S_OPEN_DARKPORTAL, c *player.Player) (result *fight.S2C_OPEN_DARKPORTAL, err errorcode.ErrorCode) {
|
||||||
|
result = &fight.S2C_OPEN_DARKPORTAL{}
|
||||||
|
|
||||||
|
towerBosses := service.NewTower110Service().Boss(uint32(data.Level))
|
||||||
|
bossConfig, ok := firstTowerBossConfig(towerBosses)
|
||||||
|
if !ok {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
bosses := service.NewBossService().Get(bossConfig.BossIds[0])
|
||||||
|
if len(bosses) == 0 {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
result.CurBossID = uint32(bosses[0].MonID)
|
||||||
|
c.CurDark = uint32(data.Level)
|
||||||
|
defer c.GetSpace().LeaveMap(c)
|
||||||
|
return result, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreshChoiceFightLevel 处理玩家选择挑战模式(试炼之塔或勇者之塔)
|
||||||
|
func (h Controller) FreshChoiceFightLevel(data *C2S_FRESH_CHOICE_FIGHT_LEVEL, c *player.Player) (result *fight.S2C_FreshChoiceLevelRequestInfo, err errorcode.ErrorCode) {
|
||||||
|
result = &fight.S2C_FreshChoiceLevelRequestInfo{}
|
||||||
|
c.Info.CurrentFreshStage = utils.Max(c.Info.CurrentFreshStage, 1)
|
||||||
|
c.Info.CurrentStage = utils.Max(c.Info.CurrentStage, 1)
|
||||||
|
|
||||||
|
choiceState, ok := towerChoiceRuntime(c, data.Head.CMD)
|
||||||
|
if !ok {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Level > 0 {
|
||||||
|
if !canSelectTowerLevel(data.Level, *choiceState.maxLevel) {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
*choiceState.currentLevel = uint32(data.Level)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.CurFightLevel = *choiceState.currentLevel
|
||||||
|
appendTowerBossPreview(&result.BossId, choiceState.service.Boss(*choiceState.currentLevel))
|
||||||
|
|
||||||
|
atomic.StoreUint32(&c.Canmon, 0)
|
||||||
|
defer c.GetSpace().LeaveMap(c)
|
||||||
|
return result, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreshLeaveFightLevel 处理控制器请求。
|
||||||
|
func (h Controller) FreshLeaveFightLevel(data *FRESH_LEAVE_FIGHT_LEVEL, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
_ = data
|
||||||
|
defer c.GetSpace().EnterMap(c)
|
||||||
|
|
||||||
|
out := info.NewOutInfo()
|
||||||
|
copier.CopyWithOption(out, c.GetInfo(), copier.Option{DeepCopy: true})
|
||||||
|
|
||||||
|
return result, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PetTawor 处理控制器请求。
|
||||||
|
func (h Controller) PetTawor(data *StartTwarInboundInfo, c *player.Player) (result *fight.S2C_ChoiceLevelRequestInfo, err errorcode.ErrorCode) {
|
||||||
|
if err = c.CanFight(); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bossList, currentLevel, taskID, ok := towerFightBosses(c, data.Head.CMD)
|
||||||
|
if !ok {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBoss, ok := firstTowerBossConfig(bossList)
|
||||||
|
if !ok {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &fight.S2C_ChoiceLevelRequestInfo{CurFightLevel: currentLevel}
|
||||||
|
appendTowerNextBossPreview(&result.BossID, bossList)
|
||||||
|
|
||||||
|
monsterInfo, bossScript, ok := buildTowerMonsterInfo(currentBoss)
|
||||||
|
if !ok {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
|
||||||
|
c.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
|
||||||
|
|
||||||
|
ai := player.NewAI_player(monsterInfo)
|
||||||
|
ai.BossScript = bossScript
|
||||||
|
_, err = fight.NewFight(c, ai, c.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
|
||||||
|
if foi.Reason != 0 || foi.WinnerId != c.Info.UserID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleTowerFightWin(c, data.Head.CMD, taskID, currentLevel)
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func towerChoiceRuntime(c *player.Player, cmd uint32) (towerChoiceState, bool) {
|
||||||
|
switch cmd {
|
||||||
|
case towerCmdChoiceTrial:
|
||||||
|
return towerChoiceState{
|
||||||
|
currentLevel: &c.Info.CurrentFreshStage,
|
||||||
|
maxLevel: &c.Info.MaxFreshStage,
|
||||||
|
service: service.NewTower600Service(),
|
||||||
|
}, true
|
||||||
|
case towerCmdChoiceBrave:
|
||||||
|
return towerChoiceState{
|
||||||
|
currentLevel: &c.Info.CurrentStage,
|
||||||
|
maxLevel: &c.Info.MaxStage,
|
||||||
|
service: service.NewTower500Service(),
|
||||||
|
}, true
|
||||||
|
default:
|
||||||
|
return towerChoiceState{}, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func towerFightBosses(c *player.Player, cmd uint32) ([]configmodel.BaseTowerConfig, uint32, int, bool) {
|
||||||
|
switch cmd {
|
||||||
|
case towerCmdFightTrial:
|
||||||
|
return service.NewTower600Service().Boss(c.Info.CurrentFreshStage, c.Info.CurrentFreshStage+1), c.Info.CurrentFreshStage, towerTaskTrial, true
|
||||||
|
case towerCmdFightBrave:
|
||||||
|
return service.NewTower500Service().Boss(c.Info.CurrentStage, c.Info.CurrentStage+1), c.Info.CurrentStage, towerTaskBrave, true
|
||||||
|
case towerCmdFightDark:
|
||||||
|
return service.NewTower110Service().Boss(c.CurDark), c.CurDark, towerTaskDark, true
|
||||||
|
default:
|
||||||
|
return nil, 0, 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func canSelectTowerLevel(targetLevel uint, maxLevel uint32) bool {
|
||||||
|
return targetLevel == 1 || targetLevel <= uint(maxLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstTowerBossConfig(bossList []configmodel.BaseTowerConfig) (configmodel.BaseTowerConfig, bool) {
|
||||||
|
if len(bossList) == 0 || len(bossList[0].BossIds) == 0 {
|
||||||
|
return configmodel.BaseTowerConfig{}, false
|
||||||
|
}
|
||||||
|
return bossList[0], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendTowerBossPreview(dst *[]uint32, bossList []configmodel.BaseTowerConfig) {
|
||||||
|
bossConfig, ok := firstTowerBossConfig(bossList)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bosses := service.NewBossService().Get(bossConfig.BossIds[0])
|
||||||
|
for _, boss := range bosses {
|
||||||
|
*dst = append(*dst, uint32(boss.MonID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendTowerNextBossPreview(dst *[]uint32, bossList []configmodel.BaseTowerConfig) {
|
||||||
|
if len(bossList) < 2 || len(bossList[1].BossIds) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bosses := service.NewBossService().Get(bossList[1].BossIds[0])
|
||||||
|
for _, boss := range bosses {
|
||||||
|
*dst = append(*dst, uint32(boss.MonID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTowerMonsterInfo(towerBoss configmodel.BaseTowerConfig) (*model.PlayerInfo, string, bool) {
|
||||||
|
bosses := service.NewBossService().Get(towerBoss.BossIds[0])
|
||||||
|
if len(bosses) == 0 {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
monsterInfo := &model.PlayerInfo{Nick: towerBoss.Name}
|
||||||
|
for i, boss := range bosses {
|
||||||
|
monster := model.GenPetInfo(int(boss.MonID), 24, int(boss.Nature), 0, int(boss.Lv), nil, 0)
|
||||||
|
if boss.Hp != 0 {
|
||||||
|
monster.Hp = uint32(boss.Hp)
|
||||||
|
monster.MaxHp = uint32(boss.Hp)
|
||||||
|
}
|
||||||
|
|
||||||
|
for statIdx, prop := range boss.Prop {
|
||||||
|
if prop != 0 {
|
||||||
|
monster.Prop[statIdx] = prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for skillIdx := 0; skillIdx < len(monster.SkillList) && skillIdx < len(boss.SKill); skillIdx++ {
|
||||||
|
if boss.SKill[skillIdx] != 0 {
|
||||||
|
monster.SkillList[skillIdx].ID = boss.SKill[skillIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
effects := service.NewEffectService().Args(boss.Effect)
|
||||||
|
for _, effect := range effects {
|
||||||
|
monster.EffectInfo = append(monster.EffectInfo, model.PetEffectInfo{
|
||||||
|
Idx: uint16(effect.ID),
|
||||||
|
EID: gconv.Uint16(effect.Eid),
|
||||||
|
Args: gconv.Ints(effect.Args),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
monster.CatchTime = uint32(i)
|
||||||
|
monsterInfo.PetList = append(monsterInfo.PetList, *monster)
|
||||||
|
}
|
||||||
|
|
||||||
|
return monsterInfo, bosses[0].Script, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTowerFightWin(c *player.Player, cmd uint32, taskID int, currentLevel uint32) {
|
||||||
|
c.TawerCompletedTask(taskID, int(currentLevel))
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case towerCmdFightTrial:
|
||||||
|
c.Info.CurrentFreshStage++
|
||||||
|
if c.Info.CurrentFreshStage >= c.Info.MaxFreshStage {
|
||||||
|
c.Info.MaxFreshStage = c.Info.CurrentFreshStage
|
||||||
|
}
|
||||||
|
case towerCmdFightBrave:
|
||||||
|
c.Info.CurrentStage++
|
||||||
|
if c.Info.CurrentStage >= c.Info.MaxStage {
|
||||||
|
c.Info.MaxStage = c.Info.CurrentStage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
logic/controller/fight_巅峰.go
Normal file
65
logic/controller/fight_巅峰.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/rpc"
|
||||||
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/logic/service/common"
|
||||||
|
"blazing/logic/service/fight"
|
||||||
|
"blazing/logic/service/fight/pvp"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 表示"宠物王加入"的入站消息数据
|
||||||
|
type PetTOPLEVELnboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2458" struc:"skip"`
|
||||||
|
Mode uint32 //巅峰赛对战模式 19 = 普通模式单精灵 20 = 普通模式多精灵
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoINtop 处理控制器请求。
|
||||||
|
func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
err = pvp.JoinPeakQueue(c, data.Mode)
|
||||||
|
if err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if Maincontroller.RPCClient == nil || Maincontroller.RPCClient.MatchJoinOrUpdate == nil {
|
||||||
|
pvp.CancelPeakQueue(c)
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
|
}
|
||||||
|
fightMode, status, err := pvp.NormalizePeakMode(data.Mode)
|
||||||
|
if err != 0 {
|
||||||
|
pvp.CancelPeakQueue(c)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
joinPayload := rpc.PVPMatchJoinPayload{
|
||||||
|
RuntimeServerID: h.UID,
|
||||||
|
UserID: c.Info.UserID,
|
||||||
|
Nick: c.Info.Nick,
|
||||||
|
FightMode: fightMode,
|
||||||
|
Status: status,
|
||||||
|
CatchTimes: pvp.AvailableCatchTimes(c.GetPetInfo(0)),
|
||||||
|
}
|
||||||
|
if callErr := Maincontroller.RPCClient.MatchJoinOrUpdate(joinPayload); callErr != nil {
|
||||||
|
pvp.CancelPeakQueue(c)
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
|
}
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelPeakQueue 处理控制器请求。
|
||||||
|
func (h Controller) CancelPeakQueue(data *PeakQueueCancelInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if Maincontroller.RPCClient != nil && Maincontroller.RPCClient.MatchCancel != nil {
|
||||||
|
_ = Maincontroller.RPCClient.MatchCancel(c.Info.UserID)
|
||||||
|
}
|
||||||
|
pvp.CancelPeakQueue(c)
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitPeakBanPick 处理控制器请求。
|
||||||
|
func (h Controller) SubmitPeakBanPick(data *PeakBanPickSubmitInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
err = pvp.SubmitBanPick(c, data.SelectedCatchTimes, data.BanCatchTimes)
|
||||||
|
if err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
@@ -2,24 +2,27 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/modules/player/model"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"blazing/logic/service/common"
|
||||||
"blazing/logic/service/fight"
|
"blazing/logic/service/fight"
|
||||||
"blazing/logic/service/fight/info"
|
"blazing/logic/service/fight/info"
|
||||||
"blazing/logic/service/player"
|
"blazing/logic/service/player"
|
||||||
"blazing/logic/service/space"
|
"blazing/logic/service/space"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ArenaSetOwner 处理玩家占据擂台的请求
|
// ARENA_SET_OWENR 定义请求或响应数据结构。
|
||||||
// public static const ARENA_SET_OWENR:uint = 2417;
|
type ARENA_SET_OWENR struct {
|
||||||
// 如果星际擂台上无人,站到星际擂台的包
|
Head common.TomeeHeader `cmd:"2417" struc:"skip"`
|
||||||
// 前端到后端无数据内容 空包
|
}
|
||||||
// 后端到前端无数据内容 空包
|
|
||||||
// ArenaSetOwner 都需要通过2419包广播更新擂台状态
|
|
||||||
func (h Controller) ArenaSetOwner(data *fight.ARENA_SET_OWENR, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
|
||||||
|
|
||||||
if !c.CanFight() {
|
// ArenaSetOwner 处理玩家占据擂台的请求
|
||||||
return nil, errorcode.ErrorCodes.ErrPokemonNotEligible
|
// ArenaSetOwner 都需要通过2419包广播更新擂台状态
|
||||||
|
func (h Controller) ArenaSetOwner(data *ARENA_SET_OWENR, c *player.Player) (result *struct{}, err errorcode.ErrorCode) {
|
||||||
|
r := c.CanFight()
|
||||||
|
if r != 0 {
|
||||||
|
return nil, r
|
||||||
}
|
}
|
||||||
c.Fightinfo.Mode = 0 //取消队列匹配
|
c.Fightinfo.Mode = 0 //取消队列匹配
|
||||||
if atomic.CompareAndSwapUint32(&c.GetSpace().Owner.Flag, 0, 1) {
|
if atomic.CompareAndSwapUint32(&c.GetSpace().Owner.Flag, 0, 1) {
|
||||||
@@ -33,18 +36,22 @@ func (h Controller) ArenaSetOwner(data *fight.ARENA_SET_OWENR, c *player.Player)
|
|||||||
return nil, errorcode.ErrorCodes.ErrChampionExists
|
return nil, errorcode.ErrorCodes.ErrChampionExists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ARENA_FIGHT_OWENR 定义请求或响应数据结构。
|
||||||
|
type ARENA_FIGHT_OWENR struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2418" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
// ArenaFightOwner 挑战擂台的包
|
// ArenaFightOwner 挑战擂台的包
|
||||||
// 前端到后端无数据内容 空包
|
|
||||||
// 后端到前端无数据内容 空包
|
|
||||||
// 还是后端主动发送2503的包给双方前端后 等待前端加载完毕 主动发送2404包通知后端开始战斗
|
// 还是后端主动发送2503的包给双方前端后 等待前端加载完毕 主动发送2404包通知后端开始战斗
|
||||||
// ArenaFightOwner 并不会通知对方是否接受挑战。只要有人挑战就直接进入对战
|
// ArenaFightOwner 并不会通知对方是否接受挑战。只要有人挑战就直接进入对战
|
||||||
func (h Controller) ArenaFightOwner(data *fight.ARENA_FIGHT_OWENR, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) ArenaFightOwner(data1 *ARENA_FIGHT_OWENR, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
|
||||||
if !c.CanFight() {
|
r := c.CanFight()
|
||||||
return nil, errorcode.ErrorCodes.ErrNoEligiblePokemon
|
if r != 0 {
|
||||||
|
return nil, r
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Info.UserID == c.GetSpace().Owner.UserID {
|
if c.IsArenaHost() {
|
||||||
return nil, errorcode.ErrorCodes.ErrNoEligiblePokemon
|
return nil, errorcode.ErrorCodes.ErrNoEligiblePokemon
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +59,7 @@ func (h Controller) ArenaFightOwner(data *fight.ARENA_FIGHT_OWENR, c *player.Pla
|
|||||||
return nil, errorcode.ErrorCodes.ErrSystemError200007
|
return nil, errorcode.ErrorCodes.ErrSystemError200007
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.GetSpace().Owner.ARENA_Player.CanFight() {
|
if c.GetSpace().Owner.ARENA_Player.CanFight() != 0 {
|
||||||
c.GetSpace().Owner.Set(c)
|
c.GetSpace().Owner.Set(c)
|
||||||
c.GetSpace().Broadcast(c, 2419, &c.GetSpace().Owner)
|
c.GetSpace().Broadcast(c, 2419, &c.GetSpace().Owner)
|
||||||
c.SendPackCmd(2419, &c.GetSpace().Owner)
|
c.SendPackCmd(2419, &c.GetSpace().Owner)
|
||||||
@@ -64,7 +71,7 @@ func (h Controller) ArenaFightOwner(data *fight.ARENA_FIGHT_OWENR, c *player.Pla
|
|||||||
c.Fightinfo.Mode = info.BattleMode.SINGLE_MODE
|
c.Fightinfo.Mode = info.BattleMode.SINGLE_MODE
|
||||||
c.Fightinfo.Status = info.BattleMode.FIGHT_ARENA
|
c.Fightinfo.Status = info.BattleMode.FIGHT_ARENA
|
||||||
|
|
||||||
_, err = fight.NewFight(c, c.GetSpace().Owner.ARENA_Player, func(foi info.FightOverInfo) { //我方邀请擂主挑战,我方先手
|
_, err = fight.NewFight(c, c.GetSpace().Owner.ARENA_Player, c.Info.PetList, c.GetSpace().Owner.ARENA_Player.GetInfo().PetList, func(foi model.FightOverInfo) { //我方邀请擂主挑战,我方先手
|
||||||
|
|
||||||
if foi.Reason != 0 && foi.WinnerId == c.GetInfo().UserID { //异常退出
|
if foi.Reason != 0 && foi.WinnerId == c.GetInfo().UserID { //异常退出
|
||||||
|
|
||||||
@@ -75,12 +82,36 @@ func (h Controller) ArenaFightOwner(data *fight.ARENA_FIGHT_OWENR, c *player.Pla
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if foi.Reason == 0 { //异常退出
|
if foi.Reason == 0 { //正常获胜
|
||||||
|
// addev := int64(int(1) * int(cool.Connected) * int(c.GetSpace().Owner.HostWins) * (int(c.GetSpace().User.Count()) / int(cool.Connected)))
|
||||||
|
addev := int64(int(2) * int(c.GetSpace().Owner.HostWins) * (int(c.GetSpace().User.Count())))
|
||||||
if foi.WinnerId == c.GetInfo().UserID {
|
if foi.WinnerId == c.GetInfo().UserID {
|
||||||
c.Info.MaxArenaWins += 1
|
c.Info.MaxArenaWins += 1
|
||||||
|
if addev != 0 {
|
||||||
|
c.Info.EVPool += addev
|
||||||
|
|
||||||
|
rewards := &info.S2C_GET_BOSS_MONSTER{}
|
||||||
|
rewards.AddItem(9, uint32(addev))
|
||||||
|
c.SendPackCmd(8004, rewards) //发送EV
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
c.GetSpace().Owner.ARENA_Player.GetInfo().MaxArenaWins += 1
|
oper := c.GetSpace().Owner.ARENA_Player
|
||||||
|
if oper != nil {
|
||||||
|
if oper.GetInfo() != nil {
|
||||||
|
c.GetSpace().Owner.ARENA_Player.GetInfo().MaxArenaWins += 1
|
||||||
|
if addev != 0 {
|
||||||
|
c.GetSpace().Owner.ARENA_Player.GetInfo().EVPool += addev
|
||||||
|
|
||||||
|
rewards := &info.S2C_GET_BOSS_MONSTER{}
|
||||||
|
rewards.AddItem(9, uint32(addev))
|
||||||
|
c.GetSpace().Owner.ARENA_Player.SendPackCmd(8004, rewards) //发送EV
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -108,17 +139,15 @@ func (h Controller) ArenaFightOwner(data *fight.ARENA_FIGHT_OWENR, c *player.Pla
|
|||||||
// ArenaGetInfo 获取星际擂台信息的包 进入空间站地图前端会发送请求包 或者 有人站到星际擂台上后 广播回包
|
// ArenaGetInfo 获取星际擂台信息的包 进入空间站地图前端会发送请求包 或者 有人站到星际擂台上后 广播回包
|
||||||
// 前端到后端无数据内容
|
// 前端到后端无数据内容
|
||||||
// ArenaGetInfo 后端到前端
|
// ArenaGetInfo 后端到前端
|
||||||
func (h Controller) ArenaGetInfo(data *fight.ARENA_GET_INFO, c *player.Player) (result *space.ARENA, err errorcode.ErrorCode) {
|
func (h Controller) ArenaGetInfo(data *ARENA_GET_INFO, c *player.Player) (result *space.ARENA, err errorcode.ErrorCode) {
|
||||||
|
|
||||||
result = &c.GetSpace().Owner
|
result = &c.GetSpace().Owner
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArenaUpfight 放弃擂台挑战的包
|
// ArenaUpfight 放弃擂台挑战的包
|
||||||
// 前端到后端无数据内容
|
|
||||||
// 后端到前端无数据内容
|
|
||||||
// ArenaUpfight 都需要通过2419包广播更新擂台状态
|
// ArenaUpfight 都需要通过2419包广播更新擂台状态
|
||||||
func (h Controller) ArenaUpfight(data *fight.ARENA_UPFIGHT, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) ArenaUpfight(data *ARENA_UPFIGHT, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
//原子操作,修改擂台状态
|
//原子操作,修改擂台状态
|
||||||
if atomic.LoadUint32(&c.GetSpace().Owner.UserID) != c.GetInfo().UserID { //说明已经有人了
|
if atomic.LoadUint32(&c.GetSpace().Owner.UserID) != c.GetInfo().UserID { //说明已经有人了
|
||||||
return nil, errorcode.ErrorCodes.ErrChampionCannotCancel
|
return nil, errorcode.ErrorCodes.ErrChampionCannotCancel
|
||||||
@@ -140,7 +169,7 @@ func (h Controller) ArenaUpfight(data *fight.ARENA_UPFIGHT, c *player.Player) (r
|
|||||||
// 后端到前端无数据内容
|
// 后端到前端无数据内容
|
||||||
// public static const ARENA_OWENR_OUT:uint = 2423;
|
// public static const ARENA_OWENR_OUT:uint = 2423;
|
||||||
// ArenaOwnerAcce 此包不清楚具体怎么触发 但已知此包为后端主动发送。不清楚什么情况下回用到
|
// ArenaOwnerAcce 此包不清楚具体怎么触发 但已知此包为后端主动发送。不清楚什么情况下回用到
|
||||||
func (h Controller) ArenaOwnerAcce(data *fight.ARENA_OWENR_ACCE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) ArenaOwnerAcce(data *ARENA_OWENR_ACCE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
|
||||||
s := c.GetSpace()
|
s := c.GetSpace()
|
||||||
|
|
||||||
198
logic/controller/inbound_fight.go
Normal file
198
logic/controller/inbound_fight.go
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import "blazing/logic/service/common"
|
||||||
|
|
||||||
|
// FightNpcMonsterInboundInfo 定义请求或响应数据结构。
|
||||||
|
type FightNpcMonsterInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2408" struc:"skip"`
|
||||||
|
Number uint32 `fieldDesc:"地图刷新怪物结构体对应的序号 1 - 9 的位置序号" `
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChallengeBossInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ChallengeBossInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2411" struc:"skip"`
|
||||||
|
BossId uint32 `json:"bossId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadyToFightInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ReadyToFightInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2404" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupReadyFightFinishInboundInfo 旧组队协议准备完成。
|
||||||
|
type GroupReadyFightFinishInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7556" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupUseSkillInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7558" struc:"skip"`
|
||||||
|
ActorIndex uint8
|
||||||
|
TargetSide uint8
|
||||||
|
TargetPos uint8
|
||||||
|
SkillId uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupUseItemInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7562" struc:"skip"`
|
||||||
|
ActorIndex uint8
|
||||||
|
ItemId uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupChangePetInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7563" struc:"skip"`
|
||||||
|
ActorIndex uint8
|
||||||
|
CatchTime uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupEscapeInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7565" struc:"skip"`
|
||||||
|
ActorIndex uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupFightWinCloseInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7574" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupFightTimeoutExitInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7587" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapeFightInboundInfo 定义请求或响应数据结构。
|
||||||
|
type EscapeFightInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2410" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartPetWarInboundInfo 定义请求或响应数据结构。
|
||||||
|
type StartPetWarInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2431" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTwarInboundInfo 定义请求或响应数据结构。
|
||||||
|
type StartTwarInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2429|2415|2425" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ARENA_GET_INFO 定义请求或响应数据结构。
|
||||||
|
type ARENA_GET_INFO struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2419" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ARENA_UPFIGHT 定义请求或响应数据结构。
|
||||||
|
type ARENA_UPFIGHT struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2420" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ARENA_OWENR_ACCE 定义请求或响应数据结构。
|
||||||
|
type ARENA_OWENR_ACCE struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2422" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PetKingJoinInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PetKingJoinInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2413" struc:"skip"`
|
||||||
|
Type uint32
|
||||||
|
FightType uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeakQueueCancelInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PeakQueueCancelInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2459" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeakBanPickSubmitInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PeakBanPickSubmitInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2460" struc:"skip"`
|
||||||
|
|
||||||
|
SelectedCatchTimesLen uint32 `struc:"sizeof=SelectedCatchTimes"`
|
||||||
|
SelectedCatchTimes []uint32 `json:"selectedCatchTimes"`
|
||||||
|
|
||||||
|
BanCatchTimesLen uint32 `struc:"sizeof=BanCatchTimes"`
|
||||||
|
BanCatchTimes []uint32 `json:"banCatchTimes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFightInviteInboundInfo 定义请求或响应数据结构。
|
||||||
|
type HandleFightInviteInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2403" struc:"skip"`
|
||||||
|
UserID uint32 `json:"userId" codec:"userId,uint"`
|
||||||
|
Flag uint32 `json:"flag" codec:"flag,uint"`
|
||||||
|
Mode uint32 `json:"mode" codec:"mode,uint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InviteToFightInboundInfo 定义请求或响应数据结构。
|
||||||
|
type InviteToFightInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2401" struc:"skip"`
|
||||||
|
UserID uint32
|
||||||
|
Mode uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// InviteFightCancelInboundInfo 定义请求或响应数据结构。
|
||||||
|
type InviteFightCancelInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2402" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseSkillInInfo 定义请求或响应数据结构。
|
||||||
|
type UseSkillInInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2405" struc:"skip"`
|
||||||
|
SkillId uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseSkillAtInboundInfo 定义请求或响应数据结构。
|
||||||
|
type UseSkillAtInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7505" struc:"skip"`
|
||||||
|
SkillId uint32 `json:"skillId"`
|
||||||
|
ActorIndex uint8 `json:"actorIndex"`
|
||||||
|
TargetIndex uint8 `json:"targetIndex"`
|
||||||
|
TargetRelation uint8 `json:"targetRelation"`
|
||||||
|
AtkType uint8 `json:"atkType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePetInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ChangePetInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2407" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catchTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatchMonsterInboundInfo 定义请求或响应数据结构。
|
||||||
|
type CatchMonsterInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2409" struc:"skip"`
|
||||||
|
CapsuleId uint32 `json:"capsuleId" fieldDescription:"胶囊id" uint:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPercentInboundInfo 定义请求或响应数据结构。
|
||||||
|
type LoadPercentInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2441" struc:"skip"`
|
||||||
|
Percent uint32 `fieldDescription:"加载百分比"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsePetItemInboundInfo 定义请求或响应数据结构。
|
||||||
|
type UsePetItemInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2406" struc:"skip"`
|
||||||
|
CatchTime uint32 `description:"精灵捕获时间" codec:"catchTime"`
|
||||||
|
ItemId uint32 `description:"使用的物品ID" codec:"itemId"`
|
||||||
|
Reversed1 uint32 `description:"填充字段 0" codec:"reversed1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatInfo 定义请求或响应数据结构。
|
||||||
|
type ChatInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"50002" struc:"skip"`
|
||||||
|
Reserve uint32 `json:"reserve" fieldDescription:"填充 默认值为0" uint:"true"`
|
||||||
|
MessageLen uint32 `struc:"sizeof=Message"`
|
||||||
|
Message string `json:"message" fieldDescription:"消息内容, 结束符为utf-8的数字0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_FRESH_CHOICE_FIGHT_LEVEL 定义请求或响应数据结构。
|
||||||
|
type C2S_FRESH_CHOICE_FIGHT_LEVEL struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2428|2414" struc:"skip"`
|
||||||
|
Level uint `json:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_OPEN_DARKPORTAL 定义请求或响应数据结构。
|
||||||
|
type C2S_OPEN_DARKPORTAL struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2424" struc:"skip"`
|
||||||
|
Level uint32 `json:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FRESH_LEAVE_FIGHT_LEVEL 定义请求或响应数据结构。
|
||||||
|
type FRESH_LEAVE_FIGHT_LEVEL struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2430|2416|2426" struc:"skip"`
|
||||||
|
}
|
||||||
61
logic/controller/inbound_friend_task.go
Normal file
61
logic/controller/inbound_friend_task.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import "blazing/logic/service/common"
|
||||||
|
|
||||||
|
// SeeOnlineInboundInfo 定义请求或响应数据结构。
|
||||||
|
type SeeOnlineInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2157" struc:"skip"`
|
||||||
|
UserIdsLen uint32 `json:"userIdsLen" struc:"sizeof=UserIds"`
|
||||||
|
UserIds []uint32 `json:"userIds" `
|
||||||
|
}
|
||||||
|
|
||||||
|
// FriendAddInboundInfo 定义请求或响应数据结构。
|
||||||
|
type FriendAddInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2151" struc:"skip"`
|
||||||
|
UserID uint32 `json:"userID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FriendAnswerInboundInfo 定义请求或响应数据结构。
|
||||||
|
type FriendAnswerInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2152" struc:"skip"`
|
||||||
|
UserID uint32 `json:"userID"`
|
||||||
|
Flag uint32 `json:"flag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FriendRemoveInboundInfo 定义请求或响应数据结构。
|
||||||
|
type FriendRemoveInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2153" struc:"skip"`
|
||||||
|
UserID uint32 `json:"userID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptTaskInboundInfo 定义请求或响应数据结构。
|
||||||
|
type AcceptTaskInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2201|2231" struc:"skip"`
|
||||||
|
TaskId uint32 `json:"taskId" description:"任务ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTaskBufInboundInfo 定义请求或响应数据结构。
|
||||||
|
type AddTaskBufInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2204|2235" struc:"skip"`
|
||||||
|
TaskId uint32 `json:"taskId" description:"任务ID"`
|
||||||
|
TaskList []uint32 `struc:"[20]byte"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteTaskInboundInfo 定义请求或响应数据结构。
|
||||||
|
type CompleteTaskInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2202|2233" struc:"skip"`
|
||||||
|
TaskId uint32 `json:"taskId" description:"任务ID"`
|
||||||
|
OutState uint32 `json:"outState" `
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTaskBufInboundInfo 定义请求或响应数据结构。
|
||||||
|
type GetTaskBufInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2203|2234" struc:"skip"`
|
||||||
|
TaskId uint32 `json:"taskId" description:"任务ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTaskInboundInfo 定义请求或响应数据结构。
|
||||||
|
type DeleteTaskInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2205|2232" struc:"skip"`
|
||||||
|
TaskId uint32 `json:"taskId" description:"任务ID"`
|
||||||
|
}
|
||||||
102
logic/controller/inbound_item.go
Normal file
102
logic/controller/inbound_item.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import "blazing/logic/service/common"
|
||||||
|
|
||||||
|
// BuyInboundInfo 定义请求或响应数据结构。
|
||||||
|
type BuyInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2601" struc:"skip"`
|
||||||
|
ItemId int64 `struc:"uint32"`
|
||||||
|
Count int64 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuyMultiInboundInfo 定义请求或响应数据结构。
|
||||||
|
type BuyMultiInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2606" struc:"skip"`
|
||||||
|
ItemListLen uint32 `struc:"sizeof=ItemIds"`
|
||||||
|
ItemIds []uint32 `json:"itemIds" description:"购买的物品ID列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_GOLD_BUY_PRODUCT 定义请求或响应数据结构。
|
||||||
|
type C2S_GOLD_BUY_PRODUCT struct {
|
||||||
|
Head common.TomeeHeader `cmd:"1104" struc:"skip"`
|
||||||
|
Type uint32 `json:"type"`
|
||||||
|
ProductID uint32 `json:"product_id"`
|
||||||
|
Count int64 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemListInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ItemListInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2605|4475" struc:"skip"`
|
||||||
|
Param1 uint32
|
||||||
|
Param2 uint32
|
||||||
|
Param3 uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoldOnlineRemainInboundInfo 定义请求或响应数据结构。
|
||||||
|
type GoldOnlineRemainInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"1105|1106" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpTotalRemainInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ExpTotalRemainInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2319" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePlayerClothInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ChangePlayerClothInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2604" struc:"skip"`
|
||||||
|
ClothesLen uint32 `struc:"sizeof=ClothList" fieldDesc:"穿戴装备的信息" json:"clothes_len"`
|
||||||
|
ClothList []uint32 `description:"玩家装备列表" codec:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TalkCountInboundInfo 定义请求或响应数据结构。
|
||||||
|
type TalkCountInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2701" struc:"skip"`
|
||||||
|
ID uint32 `description:"奖品的Type, 即ID" codec:"uint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TalkCateInboundInfo 定义请求或响应数据结构。
|
||||||
|
type TalkCateInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2702" struc:"skip"`
|
||||||
|
ID uint32 `description:"奖品的Type, 即ID" codec:"uint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_USE_PET_ITEM_OUT_OF_FIGHT 定义请求或响应数据结构。
|
||||||
|
type C2S_USE_PET_ITEM_OUT_OF_FIGHT struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2326" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catch_time"`
|
||||||
|
ItemID int32 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_PET_RESET_NATURE 定义请求或响应数据结构。
|
||||||
|
type C2S_PET_RESET_NATURE struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2343" struc:"skip"`
|
||||||
|
CatchTime uint32
|
||||||
|
Nature uint32
|
||||||
|
ItemId uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_ITEM_SALE 定义请求或响应数据结构。
|
||||||
|
type C2S_ITEM_SALE struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2602" struc:"skip"`
|
||||||
|
ItemId uint32
|
||||||
|
Amount uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_USE_SPEEDUP_ITEM 定义请求或响应数据结构。
|
||||||
|
type C2S_USE_SPEEDUP_ITEM struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2327" struc:"skip"`
|
||||||
|
ItemID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_USE_ENERGY_XISHOU 定义请求或响应数据结构。
|
||||||
|
type C2S_USE_ENERGY_XISHOU struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2331" struc:"skip"`
|
||||||
|
ItemID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_USE_AUTO_FIGHT_ITEM 定义请求或响应数据结构。
|
||||||
|
type C2S_USE_AUTO_FIGHT_ITEM struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2329" struc:"skip"`
|
||||||
|
ItemID uint32
|
||||||
|
}
|
||||||
118
logic/controller/inbound_map_room_nono.go
Normal file
118
logic/controller/inbound_map_room_nono.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/logic/service/common"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnterMapInboundInfo 定义请求或响应数据结构。
|
||||||
|
type EnterMapInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2001" struc:"skip"`
|
||||||
|
MapType uint32
|
||||||
|
MapId uint32
|
||||||
|
Point model.Pos `fieldDesc:"直接给坐标x,y"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMapHotInboundInfo 定义请求或响应数据结构。
|
||||||
|
type GetMapHotInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"1004" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeaveMapInboundInfo 定义请求或响应数据结构。
|
||||||
|
type LeaveMapInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2002" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMapPlayerInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ListMapPlayerInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2003" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttackBossInboundInfo 定义请求或响应数据结构。
|
||||||
|
type AttackBossInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2412" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalkInInfo 定义请求或响应数据结构。
|
||||||
|
type WalkInInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2101" struc:"skip"`
|
||||||
|
Flag uint32
|
||||||
|
Point model.Pos `fieldDesc:"直接给坐标x,y"`
|
||||||
|
PathLen uint32 `struc:"sizeof=Path"`
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FitmentUseringInboundInfo 定义请求或响应数据结构。
|
||||||
|
type FitmentUseringInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"10006" struc:"skip"`
|
||||||
|
TargetUserID uint32 `json:"targetUserId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PetRoomListInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PetRoomListInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2324" struc:"skip"`
|
||||||
|
TargetUserID uint32 `json:"targetUserId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FitmentAllInboundEmpty 定义请求或响应数据结构。
|
||||||
|
type FitmentAllInboundEmpty struct {
|
||||||
|
Head common.TomeeHeader `cmd:"10007" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SET_FITMENT 定义请求或响应数据结构。
|
||||||
|
type SET_FITMENT struct {
|
||||||
|
Head common.TomeeHeader `cmd:"10008" struc:"skip"`
|
||||||
|
RoomID uint32 `json:"roomID"`
|
||||||
|
FitmentsLen uint32 `json:"fitmentsLen" struc:"sizeof=Fitments"`
|
||||||
|
Fitments []model.FitmentShowInfo `json:"usedList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_PetShowList 定义请求或响应数据结构。
|
||||||
|
type C2S_PetShowList struct {
|
||||||
|
CatchTime uint32 `json:"catchTime"`
|
||||||
|
PetID uint32 `json:"petID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_PET_ROOM_SHOW 定义请求或响应数据结构。
|
||||||
|
type C2S_PET_ROOM_SHOW struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2323" struc:"skip"`
|
||||||
|
PetShowInfoLen uint32 `json:"PetShowInfoLen" struc:"sizeof=PetShowList"`
|
||||||
|
PetShowList []C2S_PetShowList `json:"PetShowList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_RoomPetInfo 定义请求或响应数据结构。
|
||||||
|
type C2S_RoomPetInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2325" struc:"skip"`
|
||||||
|
UserID uint32 `json:"userID"`
|
||||||
|
CatchTime uint32 `json:"catchTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_BUY_FITMENT 定义请求或响应数据结构。
|
||||||
|
type C2S_BUY_FITMENT struct {
|
||||||
|
Head common.TomeeHeader `cmd:"10004" struc:"skip"`
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Count uint32 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NonoInboundInfo 定义请求或响应数据结构。
|
||||||
|
type NonoInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"9003" struc:"skip"`
|
||||||
|
UserID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// NonoFollowOrHomeInInfo 定义请求或响应数据结构。
|
||||||
|
type NonoFollowOrHomeInInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"9019" struc:"skip"`
|
||||||
|
Flag uint32 `fieldDescription:"1为跟随 0为收回 且如果为收回 那么后续结构不需要发送" uint:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchFlyingInboundInfo 定义请求或响应数据结构。
|
||||||
|
type SwitchFlyingInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2112" struc:"skip"`
|
||||||
|
Type uint32 `description:"开关, 0为取消飞行模式, 大于0为开启飞行模式" codec:"auto" uint:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PetCureInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PetCureInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2306" struc:"skip"`
|
||||||
|
}
|
||||||
114
logic/controller/inbound_pet.go
Normal file
114
logic/controller/inbound_pet.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import "blazing/logic/service/common"
|
||||||
|
|
||||||
|
// GetPetInfoInboundInfo 定义请求或响应数据结构。
|
||||||
|
type GetPetInfoInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2301" struc:"skip"`
|
||||||
|
CatchTime uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserBagPetInfoInboundEmpty 定义请求或响应数据结构。
|
||||||
|
type GetUserBagPetInfoInboundEmpty struct {
|
||||||
|
Head common.TomeeHeader `cmd:"4483" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SavePetBagOrderInboundInfo 定义请求或响应数据结构。
|
||||||
|
type SavePetBagOrderInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"4484" struc:"skip"`
|
||||||
|
|
||||||
|
PetListLen uint32 `struc:"int32,sizeof=PetList"`
|
||||||
|
PetList []uint32
|
||||||
|
BackupPetListLen uint32 `struc:"int32,sizeof=BackupPetList"`
|
||||||
|
BackupPetList []uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// PetReleaseInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PetReleaseInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2304" struc:"skip"`
|
||||||
|
CatchTime uint32
|
||||||
|
Flag uint32 `json:"flag" fieldDescription:"0为放入仓库,1为放入背包" autoCodec:"true" uint:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PetShowInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PetShowInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2305" struc:"skip"`
|
||||||
|
CatchTime uint32 `codec:"catchTime" inboundMessageType:"Pet_Show"`
|
||||||
|
Flag uint32 `codec:"flag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PetOneCureInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PetOneCureInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2310" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PET_ROWEI 定义请求或响应数据结构。
|
||||||
|
type PET_ROWEI struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2321" struc:"skip"`
|
||||||
|
ID uint32
|
||||||
|
CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PET_RETRIEVE 定义请求或响应数据结构。
|
||||||
|
type PET_RETRIEVE struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2322" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PetDefaultInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PetDefaultInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2308" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true" autoCodec:"true" inboundMessageType:"Pet_Default"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PetSetExpInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PetSetExpInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2318" struc:"skip"`
|
||||||
|
CatchTime uint32 `fieldDescription:"精灵获取时间" uint:"true" autoCodec:"true"`
|
||||||
|
Exp int64 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PetBargeListInboundInfo 定义请求或响应数据结构。
|
||||||
|
type PetBargeListInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2309" struc:"skip"`
|
||||||
|
StartPetId uint32 `description:"开始精灵id" codec:"startPetId"`
|
||||||
|
EndPetId uint32 `description:"结束精灵id" codec:"endPetId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeSkillInfo 定义请求或响应数据结构。
|
||||||
|
type ChangeSkillInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2312" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catchTime"`
|
||||||
|
Reserved uint32 `json:"reserved"`
|
||||||
|
Reserved1 uint32 `json:"reserved1"`
|
||||||
|
HasSkill uint32 `json:"hasSkill"`
|
||||||
|
ReplaceSkill uint32 `json:"replaceSkill"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_Skill_Sort 定义请求或响应数据结构。
|
||||||
|
type C2S_Skill_Sort struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2328" struc:"skip"`
|
||||||
|
CapTm uint32 `json:"capTm"`
|
||||||
|
Skill [4]uint32 `json:"skill_1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPetLearnableSkillsInboundInfo 查询当前精灵可学习技能(含额外技能ExtSKill)
|
||||||
|
type GetPetLearnableSkillsInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"52312" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catchTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommitPetSkillsInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"52313" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catchTime"`
|
||||||
|
Skill [4]uint32 `json:"skill"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type C2S_PetFusion struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2351" struc:"skip"`
|
||||||
|
Mcatchtime uint32 `json:"mcatchtime" msgpack:"mcatchtime"`
|
||||||
|
Auxcatchtime uint32 `json:"auxcatchtime" msgpack:"auxcatchtime"`
|
||||||
|
Item1 [4]uint32 `json:"item1" msgpack:"item1"`
|
||||||
|
GoldItem1 [2]uint32 `json:"gold_item1" msgpack:"gold_item1"`
|
||||||
|
}
|
||||||
111
logic/controller/inbound_user.go
Normal file
111
logic/controller/inbound_user.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/cool"
|
||||||
|
"blazing/logic/service/common"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MAIN_LOGIN_IN 定义请求或响应数据结构。
|
||||||
|
type MAIN_LOGIN_IN struct {
|
||||||
|
Head common.TomeeHeader `cmd:"1001" struc:"skip"`
|
||||||
|
Sid []byte `struc:"[16]byte"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheakSession 校验登录session。
|
||||||
|
func (l *MAIN_LOGIN_IN) CheakSession() (bool, uint32) {
|
||||||
|
t1 := hex.EncodeToString(l.Sid)
|
||||||
|
r, err := cool.CacheManager.Get(context.Background(), fmt.Sprintf("session:%d", l.Head.UserID))
|
||||||
|
if err != nil {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
if r.String() != t1 {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
crc32Table := crc32.MakeTable(crc32.IEEE)
|
||||||
|
crcValue := crc32.Checksum([]byte(l.Sid), crc32Table)
|
||||||
|
cool.CacheManager.Remove(context.Background(), fmt.Sprintf("session:%d", l.Head.UserID))
|
||||||
|
return true, crcValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimUserInfoInboundInfo 定义请求或响应数据结构。
|
||||||
|
type SimUserInfoInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2051" struc:"skip"`
|
||||||
|
UserId uint32 `fieldDescription:"米米号" uint:"true" codec:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoreUserInfoInboundInfo 定义请求或响应数据结构。
|
||||||
|
type MoreUserInfoInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2052" struc:"skip"`
|
||||||
|
UserId uint32 `fieldDescription:"米米号" uint:"true" codec:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AimatInboundInfo 定义请求或响应数据结构。
|
||||||
|
type AimatInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2104" struc:"skip"`
|
||||||
|
ItemId uint32 `description:"物品id 射击激光 物品id为0" codec:"auto" uint:"true"`
|
||||||
|
ShootType uint32 `description:"射击类型 未知 给0" codec:"auto" uint:"true"`
|
||||||
|
Point model.Pos `description:"射击的坐标 x y" codec:"auto"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ChatInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2102" struc:"skip"`
|
||||||
|
Reserve uint32 `json:"reserve" fieldDescription:"填充 默认值为0" uint:"true"`
|
||||||
|
MessageLen uint32 `struc:"sizeof=Message"`
|
||||||
|
Message string `json:"message" fieldDescription:"消息内容, 结束符为utf-8的数字0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeColorInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ChangeColorInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2063" struc:"skip"`
|
||||||
|
Color uint32 `codec:"color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeDoodleInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ChangeDoodleInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2062" struc:"skip"`
|
||||||
|
Id uint32 `codec:"id"`
|
||||||
|
Color uint32 `codec:"color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeNONOColorInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ChangeNONOColorInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"9012" struc:"skip"`
|
||||||
|
Color uint32 `codec:"color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2SDanceAction 定义请求或响应数据结构。
|
||||||
|
type C2SDanceAction struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2103" struc:"skip"`
|
||||||
|
Reserve uint32 `struc:"uint32,big"`
|
||||||
|
Type uint32 `struc:"uint32,big"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2SPEOPLE_TRANSFROM 定义请求或响应数据结构。
|
||||||
|
type C2SPEOPLE_TRANSFROM struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2111" struc:"skip"`
|
||||||
|
SuitID uint32 `struc:"uint32,big"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePlayerNameInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ChangePlayerNameInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2061" struc:"skip"`
|
||||||
|
Nickname string `struc:"[16]byte"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeTitleInboundInfo 定义请求或响应数据结构。
|
||||||
|
type ChangeTitleInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"3404" struc:"skip"`
|
||||||
|
TileID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2S_GET_GIFT_COMPLETE 定义请求或响应数据结构。
|
||||||
|
type C2S_GET_GIFT_COMPLETE struct {
|
||||||
|
Head common.TomeeHeader `cmd:"2801" struc:"skip"`
|
||||||
|
PassText string `struc:"[16]byte"`
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user