Compare commits
863 Commits
179e50f94d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
30c89dcd2a | ||
|
|
7ba3ff191e | ||
|
|
0171174b1b | ||
|
|
2ebb2affb3 | ||
|
|
35abc16bc3 | ||
|
|
74f07a1d00 | ||
|
|
09a295db34 | ||
|
|
e65b50dd33 | ||
|
|
7589306faa | ||
|
|
66e59be70b | ||
|
|
40057b698d | ||
|
|
76ee66f616 | ||
|
|
4b83346f36 | ||
|
|
55fbcd6fc3 | ||
|
|
b049e129c5 | ||
|
|
af92c89e1c | ||
|
|
796b55ffad | ||
|
|
56ec5ba40f | ||
|
|
b851796607 | ||
|
|
730a1db487 | ||
|
|
8007a2a2b4 | ||
|
|
e6ef348fcc | ||
|
|
646a5ba835 | ||
|
|
48743209f5 | ||
|
|
f49c38e6d4 | ||
|
|
0c2a7944e5 | ||
|
|
94c9c4e44c | ||
|
|
cf332d916f | ||
|
|
37fe80c241 | ||
|
|
dc647cdfbb | ||
|
|
b25f033939 | ||
|
|
cdb7cec4ad | ||
|
|
acc9bcf6ff | ||
|
|
4592e1d2f8 | ||
|
|
45175d9a60 | ||
|
|
ff675f9430 | ||
|
|
aa58dfdbb2 | ||
|
|
e4fccd7050 | ||
|
|
49bd303f47 | ||
|
|
95ccb99179 | ||
|
|
85a90d263a | ||
|
|
65dce34473 | ||
|
|
361b620148 | ||
|
|
836bafc6f9 | ||
|
|
da3655508d | ||
|
|
dc188d83db | ||
|
|
b12dd742e6 | ||
|
|
53902a7a2e | ||
|
|
8ebc35d244 | ||
|
|
faf39ffdef | ||
|
|
ec866dda58 | ||
|
|
bf08cb3598 | ||
|
|
210a14520f | ||
|
|
ab06f822e5 | ||
|
|
d3d6b65e41 | ||
|
|
d6f8e9dbf7 | ||
|
|
dd2a267194 | ||
|
|
0c6d22d3f4 | ||
|
|
d07f04bafc | ||
|
|
51174479ad | ||
|
|
dbf3fac2c8 | ||
|
|
3fbc30ae04 | ||
|
|
2b63e3b237 |
13
.cnb.yml
13
.cnb.yml
@@ -1,3 +1,16 @@
|
||||
$:
|
||||
vscode:
|
||||
- runner:
|
||||
cpus: 6
|
||||
docker:
|
||||
build: .ide/Dockerfile
|
||||
services:
|
||||
- vscode
|
||||
- docker
|
||||
stages:
|
||||
- name: ls
|
||||
script: ls -al
|
||||
|
||||
main:
|
||||
push:
|
||||
- runner:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -43,6 +43,8 @@ logic/logic1
|
||||
logic/logic1
|
||||
public/logic-linux-amd64
|
||||
public/login-linux-amd64
|
||||
|
||||
.cache/gomod/**
|
||||
public/login-login-linux-amd64
|
||||
public/logic_linux-amd64_1
|
||||
.cache/**
|
||||
.agents/**
|
||||
|
||||
158
.ide/Dockerfile
158
.ide/Dockerfile
@@ -1,45 +1,135 @@
|
||||
# 此文件为远程开发环境配置文件
|
||||
FROM debian:bookworm
|
||||
|
||||
# ==========================================
|
||||
# 1. 基础环境变量
|
||||
# ==========================================
|
||||
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 &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
# ==========================================
|
||||
# 2. Codex 配置 (更换时修改这里,重新 build)
|
||||
# ==========================================
|
||||
ENV CODEX_BASE_URL="https://api.jucode.cn/v1"
|
||||
|
||||
# install golang
|
||||
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
|
||||
ENV CODEX_MODEL="gpt-5.4"
|
||||
|
||||
# install code-server
|
||||
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
|
||||
ENV OPENAI_API_KEY="sk-E0ZZIFNnD0RkhMC9pT2AGMutz9vNy2VLNrgyyobT5voa81pQ"
|
||||
|
||||
# install Go Tools
|
||||
ENV GOPATH /root/go
|
||||
ENV PATH="${PATH}:${GOPATH}/bin"
|
||||
# ==========================================
|
||||
# 3. 安装系统依赖、Golang、Code-server
|
||||
# ==========================================
|
||||
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
|
||||
ENV LANG zh_CN.UTF-8
|
||||
ENV LANGUAGE zh_CN.UTF-8
|
||||
RUN cat > /root/.codex/config.toml <<EOF
|
||||
model_provider = "OpenAI"
|
||||
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",
|
||||
"mode": "auto",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": ["-id=2"],
|
||||
"args": ["-id=99"],
|
||||
|
||||
"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"]
|
||||
}
|
||||
@@ -8,18 +8,12 @@ when:
|
||||
skip_clone: true
|
||||
|
||||
|
||||
# 全局配置变量(替换占位符即可使用)
|
||||
variables:
|
||||
SCREEN_NAME: "logic_service"
|
||||
REMOTE_EXE_DIR: "/opt/logic"
|
||||
JSON_CONFIG_URL: "https://你的JSON配置地址.com/deploy.json"
|
||||
LOG_PATH: "$HOME/run.log"
|
||||
|
||||
# 流水线核心步骤:理顺依赖链,确保各步骤依赖正确
|
||||
steps:
|
||||
# ========== 1. 替代clone:拉取代码(核心依赖) ==========
|
||||
prepare:
|
||||
image: debian:bookworm
|
||||
image: alpine/git
|
||||
environment:
|
||||
# WOODPECKER_SSH_KEY:
|
||||
# from_secret: WOODPECKER_SSH_KEY
|
||||
@@ -35,18 +29,13 @@ steps:
|
||||
# - cp /etc/apt/sources.list /etc/apt/sources.list.bak
|
||||
|
||||
# 2. 清空原有内容,写入阿里云Debian bookworm镜像源(直接覆盖,无需手动编辑)
|
||||
- |
|
||||
cat > /etc/apt/sources.list << EOF
|
||||
# 阿里云Debian 12 (bookworm) 镜像源
|
||||
deb http://mirrors.aliyun.com/debian/ bookworm main contrib non-free non-free-firmware
|
||||
deb http://mirrors.aliyun.com/debian/ bookworm-updates main contrib non-free non-free-firmware
|
||||
deb http://mirrors.aliyun.com/debian/ bookworm-backports main contrib non-free non-free-firmware
|
||||
deb http://mirrors.aliyun.com/debian-security/ bookworm-security main contrib non-free non-free-firmware
|
||||
EOF
|
||||
# - echo "deb http://mirrors.aliyun.com/debian/ bookworm main contrib non-free non-free-firmware
|
||||
# deb http://mirrors.aliyun.com/debian/ bookworm-updates main contrib non-free non-free-firmware
|
||||
# deb http://mirrors.aliyun.com/debian-security/ bookworm-security main contrib non-free non-free-firmware" > /etc/apt/sources.list
|
||||
|
||||
# 3. 更新软件源缓存(使新源生效)
|
||||
- apt update -y
|
||||
- apt install -y --no-install-recommends curl git openssh-client openssl libssl-dev
|
||||
# # 3. 更新软件源缓存(使新源生效)
|
||||
# - apt update -y
|
||||
# - apt install -y --no-install-recommends ca-certificates curl git openssh-client openssl libssl-dev
|
||||
|
||||
# # 清理旧SSH文件,严格配置权限
|
||||
# - rm -rf /root/.ssh/*
|
||||
@@ -67,9 +56,11 @@ steps:
|
||||
# - ssh-keyscan -H github.com > /root/.ssh/known_hosts
|
||||
# - chmod 600 /root/.ssh/known_hosts
|
||||
# - echo "🔍 ${#CI_REPO_CLONE_SSH_URL}调试: ${CI_REPO_CLONE_SSH_URL}"
|
||||
|
||||
- git config --global core.compression 0
|
||||
- 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 clone --depth 1 --progress -v $GIT_CONFIG_URL
|
||||
# 拉取代码
|
||||
|
||||
@@ -86,6 +77,21 @@ steps:
|
||||
GO111MODULE: on
|
||||
GOSUMDB: off
|
||||
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
|
||||
- mkdir -p build
|
||||
- BIN_NAME="login_${CI_PIPELINE_CREATED}"
|
||||
@@ -99,6 +105,9 @@ steps:
|
||||
-ldflags "-s -w -buildid= -extldflags '-static'" \
|
||||
-o ./build/$BIN_NAME \
|
||||
./login
|
||||
# - |
|
||||
# strip ./build/$BIN_NAME
|
||||
# upx --best --lzma ./build/$BIN_NAME
|
||||
- |
|
||||
if [ ! -f ./build/$BIN_NAME ]; then
|
||||
echo "❌ 编译失败:产物$BIN_NAME不存在"
|
||||
@@ -115,6 +124,9 @@ steps:
|
||||
-ldflags "-s -w -buildid= -extldflags '-static'" \
|
||||
-o ./build/$BIN_NAME \
|
||||
./logic
|
||||
- |
|
||||
strip ./build/$BIN_NAME
|
||||
upx --best --lzma ./build/$BIN_NAME
|
||||
- |
|
||||
if [ ! -f ./build/$BIN_NAME ]; then
|
||||
echo "❌ 编译失败:产物$BIN_NAME不存在"
|
||||
@@ -132,13 +144,14 @@ steps:
|
||||
scp-exe-to-servers: # 与fetch-deploy-config同级,缩进2个空格
|
||||
image: appleboy/drone-scp:1.6.2 # 子元素,缩进4个空格
|
||||
settings: # 子元素,缩进4个空格
|
||||
host: 103.236.78.60 # settings内的项,缩进6个空格
|
||||
port: 29713
|
||||
username: root # 统一缩进6个空格
|
||||
password: dgaoXMPC8325 # 统一缩进6个空格
|
||||
host: &ssh_host 43.248.3.21
|
||||
port: &ssh_port 22
|
||||
username: &ssh_user root
|
||||
password: &ssh_pass KQv7yzna7BDukK
|
||||
|
||||
source:
|
||||
- blazing/build/**
|
||||
target: /opt/blazing/
|
||||
target: /ext/blazing/
|
||||
strip_components: 1 # 统一缩进6个空格
|
||||
skip_verify: true # 统一缩进6个空格
|
||||
timeout: 30s # 统一缩进6个空格
|
||||
@@ -148,13 +161,13 @@ steps:
|
||||
image: appleboy/drone-ssh:1.6.2
|
||||
depends_on: [scp-exe-to-servers]
|
||||
settings: # 子元素,缩进4个空格
|
||||
host: 103.236.78.60 # settings内的项,缩进6个空格
|
||||
port: 29713
|
||||
username: root # 统一缩进6个空格
|
||||
password: dgaoXMPC8325 # 统一缩进6个空格
|
||||
host: *ssh_host
|
||||
port: *ssh_port
|
||||
username: *ssh_user
|
||||
password: *ssh_pass
|
||||
script:
|
||||
- |
|
||||
cd /opt/blazing/build
|
||||
cd /ext/blazing/build
|
||||
ls -t login_* 2>/dev/null | head -1
|
||||
BIN_NAME=$(ls -t login_* 2>/dev/null | head -1)
|
||||
echo "BIN_NAME: $BIN_NAME"
|
||||
@@ -188,9 +201,9 @@ steps:
|
||||
# 移动logic产物到public目录
|
||||
LOGIC_BIN=$(ls -t logic_* 2>/dev/null | head -1)
|
||||
if [ -n "$LOGIC_BIN" ]; then
|
||||
mkdir -p /opt/blazing/build/public
|
||||
mv $LOGIC_BIN /opt/blazing/build/public/
|
||||
echo "✅ Logic产物已移动到 /opt/blazing/build/public/ | 文件: $(basename $LOGIC_BIN)"
|
||||
mkdir -p /ext/blazing/build/public
|
||||
mv $LOGIC_BIN /ext/blazing/build/public/
|
||||
echo "✅ Logic产物已移动到 /ext/blazing/build/public/ | 文件: $(basename $LOGIC_BIN)"
|
||||
else
|
||||
echo "⚠️ 未找到Logic产物"
|
||||
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.
|
||||
11
README.md
11
README.md
@@ -8,9 +8,18 @@
|
||||
## 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://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/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/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
@@ -17,21 +18,45 @@ type Local struct {
|
||||
func (l *Local) Upload(ctx g.Ctx) (string, error) {
|
||||
var (
|
||||
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 {
|
||||
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")
|
||||
|
||||
fileName, err := file.Save("./public/uploads/"+dir, true)
|
||||
// 保存路径:./public/uploads/年月日
|
||||
saveDir := "./public/uploads/" + dir
|
||||
// 保存文件(自动重命名避免重复)
|
||||
fileName, err := file.Save(saveDir, true)
|
||||
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) {
|
||||
|
||||
@@ -98,6 +98,11 @@ func (c *Controller) Delete(ctx context.Context, req *DeleteReq) (res *BaseRes,
|
||||
if err != nil {
|
||||
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())
|
||||
return Ok(data), err
|
||||
}
|
||||
@@ -115,6 +120,10 @@ func (c *Controller) Update(ctx context.Context, req *UpdateReq) (res *BaseRes,
|
||||
if err != nil {
|
||||
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())
|
||||
return Ok(data), err
|
||||
}
|
||||
|
||||
@@ -95,3 +95,17 @@ func Fail(message string) *BaseRes {
|
||||
// }
|
||||
// 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,116 +1,121 @@
|
||||
package coolconfig
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// cool config
|
||||
type sConfig struct {
|
||||
AutoMigrate bool `json:"auto_migrate,omitempty"` // 是否自动创建表
|
||||
Eps bool `json:"eps,omitempty"` // 是否开启eps
|
||||
File *file `json:"file,omitempty"` // 文件上传配置
|
||||
Name string `json:"name"` // 项目名称
|
||||
// LoginPort string `json:"port"`
|
||||
GameOnlineID uint16 `json:"port_bl"` //这个是命令行输入的参数
|
||||
ServerInfo ServerList
|
||||
|
||||
Address string //rpc端口
|
||||
|
||||
}
|
||||
type ServerList struct {
|
||||
OnlineID uint16 `gorm:"column:online_id;comment:'在线ID';uniqueIndex" json:"online_id"`
|
||||
//服务器名称Desc
|
||||
Name string `gorm:"comment:'服务器名称'" json:"name"`
|
||||
IP string `gorm:"type:string;comment:'服务器IP'" json:"ip"`
|
||||
Port uint16 `gorm:"comment:'端口号,通常是小整数'" json:"port"`
|
||||
IsOpen uint8 `gorm:"default:0;not null;comment:'是否开启'" json:"is_open"`
|
||||
//登录地址
|
||||
LoginAddr string `gorm:"type:string;comment:'登录地址'" json:"login_addr"`
|
||||
//账号
|
||||
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"`
|
||||
//是否测试服
|
||||
IsVip uint32 `gorm:"default:0;not null;comment:'是否为VIP服务器'" json:"is_vip"`
|
||||
//isdebug 是否本地服
|
||||
IsDebug uint8 `gorm:"default:0;comment:'是否为调试模式'" json:"is_debug"`
|
||||
//服务器异色概率设定ServerList
|
||||
ShinyRate uint8 `gorm:"default:0;comment:'异色概率'" json:"shiny_rate"`
|
||||
//服务器天气设定ServerList
|
||||
WeatherRate uint8 `gorm:"default:0;comment:'天气概率'" json:"weather_rate"`
|
||||
|
||||
//服务器属主Desc
|
||||
Owner uint32 `gorm:"comment:'服务器属主'" json:"owner"`
|
||||
Desc string `gorm:"comment:'服务器描述'" json:"desc"`
|
||||
OldScreen string `gorm:"comment:'服务器screen参数'" json:"old_screen"`
|
||||
}
|
||||
|
||||
// OSS相关配置
|
||||
type oss struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
AccessKeyID string `json:"accessKeyID"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
UseSSL bool `json:"useSSL"`
|
||||
BucketName string `json:"bucketName"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
// 文件上传配置
|
||||
type file struct {
|
||||
Mode string `json:"mode"` // 模式 local oss
|
||||
Domain string `json:"domain"` // 域名 http://
|
||||
Oss *oss `json:"oss,omitempty"`
|
||||
}
|
||||
|
||||
// NewConfig new config
|
||||
func newConfig() *sConfig {
|
||||
var ctx g.Ctx
|
||||
config := &sConfig{
|
||||
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()),
|
||||
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(),
|
||||
Domain: GetCfgWithDefault(ctx, "blazing.file.domain", g.NewVar("http://127.0.0.1:8300")).String(),
|
||||
Oss: &oss{
|
||||
Endpoint: GetCfgWithDefault(ctx, "blazing.file.oss.endpoint", g.NewVar("127.0.0.1:9000")).String(),
|
||||
AccessKeyID: GetCfgWithDefault(ctx, "blazing.file.oss.accessKeyID", g.NewVar("")).String(),
|
||||
SecretAccessKey: GetCfgWithDefault(ctx, "blazing.file.oss.secretAccessKey", g.NewVar("")).String(),
|
||||
UseSSL: GetCfgWithDefault(ctx, "blazing.file.oss.useSSL", g.NewVar(false)).Bool(),
|
||||
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
|
||||
}
|
||||
|
||||
// qiniu 七牛云配置
|
||||
type qiniu struct {
|
||||
AccessKey string `json:"ak"`
|
||||
SecretKey string `json:"sk"`
|
||||
Bucket string `json:"bucket"`
|
||||
CDN string `json:"cdn"`
|
||||
}
|
||||
|
||||
// Config config
|
||||
var Config = newConfig()
|
||||
|
||||
// GetCfgWithDefault get config with default value
|
||||
func GetCfgWithDefault(ctx g.Ctx, key string, defaultValue *g.Var) *g.Var {
|
||||
value, err := g.Cfg().GetWithEnv(ctx, key)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
if value.IsEmpty() || value.IsNil() {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
package coolconfig
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// cool config
|
||||
type sConfig struct {
|
||||
AutoMigrate bool `json:"auto_migrate,omitempty"` // 是否自动创建表
|
||||
Eps bool `json:"eps,omitempty"` // 是否开启eps
|
||||
File *file `json:"file,omitempty"` // 文件上传配置
|
||||
Name string `json:"name"` // 项目名称
|
||||
// LoginPort string `json:"port"`
|
||||
GameOnlineID uint32 `json:"port_bl"` //这个是命令行输入的参数
|
||||
ServerInfo ServerList
|
||||
|
||||
Address string //rpc端口
|
||||
|
||||
}
|
||||
type ServerList struct {
|
||||
OnlineID uint32 `gorm:"column:online_id;comment:'在线ID';uniqueIndex" json:"online_id"`
|
||||
//服务器名称Desc
|
||||
Name string `gorm:"comment:'服务器名称'" json:"name"`
|
||||
IP string `gorm:"type:string;comment:'服务器IP'" json:"ip"`
|
||||
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"`
|
||||
//账号
|
||||
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"`
|
||||
//是否测试服
|
||||
IsVip uint32 `gorm:"default:0;not null;comment:'是否为VIP服务器'" json:"is_vip"`
|
||||
//isdebug 是否本地服
|
||||
IsDebug uint8 `gorm:"default:0;comment:'是否为调试模式'" json:"is_debug"`
|
||||
|
||||
//服务器属主Desc
|
||||
Owner uint32 `gorm:"comment:'服务器属主'" json:"owner"`
|
||||
Desc string `gorm:"comment:'服务器描述'" json:"desc"`
|
||||
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 {
|
||||
Endpoint string `json:"endpoint"`
|
||||
AccessKeyID string `json:"accessKeyID"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
UseSSL bool `json:"useSSL"`
|
||||
BucketName string `json:"bucketName"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
// 文件上传配置
|
||||
type file struct {
|
||||
Mode string `json:"mode"` // 模式 local oss
|
||||
Domain string `json:"domain"` // 域名 http://
|
||||
Oss *oss `json:"oss,omitempty"`
|
||||
}
|
||||
|
||||
// NewConfig new config
|
||||
func newConfig() *sConfig {
|
||||
var ctx g.Ctx
|
||||
config := &sConfig{
|
||||
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()),
|
||||
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(),
|
||||
Domain: GetCfgWithDefault(ctx, "blazing.file.domain", g.NewVar("http://127.0.0.1:8300")).String(),
|
||||
Oss: &oss{
|
||||
Endpoint: GetCfgWithDefault(ctx, "blazing.file.oss.endpoint", g.NewVar("127.0.0.1:9000")).String(),
|
||||
AccessKeyID: GetCfgWithDefault(ctx, "blazing.file.oss.accessKeyID", g.NewVar("")).String(),
|
||||
SecretAccessKey: GetCfgWithDefault(ctx, "blazing.file.oss.secretAccessKey", g.NewVar("")).String(),
|
||||
UseSSL: GetCfgWithDefault(ctx, "blazing.file.oss.useSSL", g.NewVar(false)).Bool(),
|
||||
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
|
||||
}
|
||||
|
||||
// qiniu 七牛云配置
|
||||
type qiniu struct {
|
||||
AccessKey string `json:"ak"`
|
||||
SecretKey string `json:"sk"`
|
||||
Bucket string `json:"bucket"`
|
||||
CDN string `json:"cdn"`
|
||||
}
|
||||
|
||||
// Config config
|
||||
var Config = newConfig()
|
||||
|
||||
// GetCfgWithDefault get config with default value
|
||||
func GetCfgWithDefault(ctx g.Ctx, key string, defaultValue *g.Var) *g.Var {
|
||||
value, err := g.Cfg().GetWithEnv(ctx, key)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
if value.IsEmpty() || value.IsNil() {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func RunFunc(ctx g.Ctx, funcstring string) (err error) {
|
||||
// ClusterRunFunc 集群运行函数,如果是单机模式, 则直接运行函数
|
||||
func ClusterRunFunc(ctx g.Ctx, funcstring string) (err error) {
|
||||
if IsRedisMode {
|
||||
conn, err := g.Redis("cool").Conn(ctx)
|
||||
conn, err := Redis.Conn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -63,41 +63,3 @@ func ClusterRunFunc(ctx g.Ctx, funcstring string) (err error) {
|
||||
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 {
|
||||
Func reflect.Value //方法函数
|
||||
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 //返回体
|
||||
}
|
||||
|
||||
var CmdCache = make(map[uint32]Cmd, 0)
|
||||
var (
|
||||
Logger = glog.New()
|
||||
Cron = cronex.New() //时间轮
|
||||
|
||||
Logger = glog.New()
|
||||
Cron = cronex.New() //时间轮
|
||||
Connected int64
|
||||
)
|
||||
var Filter *sensitive.Manager
|
||||
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() {
|
||||
// 创建 IdGeneratorOptions 对象,可在构造函数中输入 WorkerId:
|
||||
Logger.SetFlags(glog.F_TIME_STD | glog.F_FILE_LONG | glog.F_ASYNC) //设置flag
|
||||
Logger.Print(ctx, "初始化日志")
|
||||
|
||||
// for i := 0; i < 600; i++ {
|
||||
// glog.Debug(context.Background(), i, "初始化雪花算法", DefaultGenerator.Get())
|
||||
// }
|
||||
|
||||
@@ -19,7 +19,7 @@ require (
|
||||
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/imroc/req/v3 v3.43.3 // indirect
|
||||
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.16.0 // indirect
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
|
||||
|
||||
@@ -3,6 +3,7 @@ package cool
|
||||
import (
|
||||
_ "blazing/contrib/drivers/pgsql"
|
||||
"blazing/cool/cooldb"
|
||||
"sync"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
@@ -10,6 +11,11 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
autoMigrateMu sync.Mutex
|
||||
autoMigrateModels []IModel
|
||||
)
|
||||
|
||||
// 初始化数据库连接供gorm使用
|
||||
func InitDB(group string) (*gorm.DB, error) {
|
||||
// var ctx context.Context
|
||||
@@ -54,9 +60,33 @@ func getDBbyModel(model IModel) *gorm.DB {
|
||||
|
||||
// 根据entity结构体创建表
|
||||
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)
|
||||
return db.AutoMigrate(model)
|
||||
if err := db.AutoMigrate(model); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
package cool
|
||||
|
||||
// 存值示例
|
||||
func AddClient(id uint16, client *ClientHandler) {
|
||||
func AddClient(id uint32, client *ClientHandler) {
|
||||
// 普通map:Clientmap[id] = client
|
||||
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]
|
||||
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 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package cool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/gogf/gf/v2/container/garray"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
@@ -11,20 +12,21 @@ import (
|
||||
)
|
||||
|
||||
type IService interface {
|
||||
ServiceAdd(ctx context.Context, req *AddReq) (data interface{}, err error) // 新增
|
||||
ServiceDelete(ctx context.Context, req *DeleteReq) (data interface{}, err error) // 删除
|
||||
ServiceUpdate(ctx context.Context, req *UpdateReq) (data interface{}, err error) // 修改
|
||||
ServiceInfo(ctx context.Context, req *InfoReq) (data interface{}, err error) // 详情
|
||||
ServiceList(ctx context.Context, req *ListReq) (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) // 新增|删除|修改前的操作
|
||||
ModifyAfter(ctx context.Context, method string, param g.MapStrAny) (err error) // 新增|删除|修改后的操作
|
||||
GetModel() IModel // 获取model
|
||||
ServiceAdd(ctx context.Context, req *AddReq) (data interface{}, err error) // 新增
|
||||
ServiceDelete(ctx context.Context, req *DeleteReq) (data sql.Result, err error) // 删除
|
||||
ServiceUpdate(ctx context.Context, req *UpdateReq) (data sql.Result, err error) // 修改
|
||||
ServiceInfo(ctx context.Context, req *InfoReq) (data interface{}, err error) // 详情
|
||||
ServiceList(ctx context.Context, req *ListReq) (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) // 新增|删除|修改前的操作
|
||||
ModifyAfter(ctx context.Context, method string, param g.MapStrAny) (err error) // 新增|删除|修改后的操作
|
||||
GetModel() IModel // 获取model
|
||||
}
|
||||
type Service struct {
|
||||
Model IModel
|
||||
ListQueryOp *QueryOp
|
||||
PageQueryOp *QueryOp
|
||||
Where func(ctx context.Context) []g.Array // 删除修改定义条件
|
||||
InsertParam func(ctx context.Context) g.MapStrAny // Add时插入参数
|
||||
Before func(ctx context.Context) (err error) // CRUD前的操作
|
||||
InfoIgnoreProperty string // Info时忽略的字段,多个字段用逗号隔开
|
||||
@@ -35,11 +37,13 @@ type Service struct {
|
||||
|
||||
// List/Add接口条件配置
|
||||
type QueryOp struct {
|
||||
FieldEQ []string // 字段等于 多个字段选择以及高级搜索都是这个
|
||||
KeyWordField []string // 模糊搜索匹配的数据库字段,对应普通搜索
|
||||
AddOrderby g.MapStrStr // 添加排序
|
||||
Where func(ctx context.Context) []g.Array // 自定义条件
|
||||
Select string // 查询字段,多个字段用逗号隔开 如: id,name 或 a.id,a.name,b.name AS bname
|
||||
FieldEQ []string // 字段等于 多个字段选择以及高级搜索都是这个
|
||||
DataFieldEQ []string // 新增:JSONB data->>'xxx' 字段 = ? 多个字段选择以及高级搜索都是这个
|
||||
KeyWordField []string // 模糊搜索匹配的数据库字段,对应普通搜索
|
||||
AddOrderby g.MapStrStr // 添加排序
|
||||
Where func(ctx context.Context) []g.Array // 自定义条件
|
||||
Select string // 查询字段,多个字段用逗号隔开 如: id,name 或 a.id,a.name,b.name AS bname
|
||||
|
||||
Join []*JoinOp // 关联查询
|
||||
Extend func(ctx g.Ctx, m *gdb.Model) *gdb.Model // 追加其他条件
|
||||
ModifyResult func(ctx g.Ctx, data interface{}) interface{} // 修改结果
|
||||
@@ -104,16 +108,31 @@ func (s *Service) ServiceAdd(ctx context.Context, req *AddReq) (data interface{}
|
||||
}
|
||||
|
||||
// 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()
|
||||
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()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
rmap := r.GetMap()
|
||||
if rmap["id"] == nil {
|
||||
@@ -135,7 +154,24 @@ func (s *Service) ServiceUpdate(ctx context.Context, req *UpdateReq) (data inter
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -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不为空 则添加查询条件
|
||||
if !r.Get("keyWord").IsEmpty() {
|
||||
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 {
|
||||
// g.DumpWithType(field)
|
||||
// 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)
|
||||
}
|
||||
@@ -218,6 +268,8 @@ func (s *Service) ServiceList(ctx context.Context, req *ListReq) (data interface
|
||||
if len(v) == 3 {
|
||||
if gconv.Bool(v[2]) {
|
||||
m.Where(v[0], v[1])
|
||||
} else {
|
||||
m.WhereNot(gconv.String(v[0]), v[1])
|
||||
}
|
||||
}
|
||||
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不为空 则添加查询条件
|
||||
if !r.Get("keyWord").IsEmpty() {
|
||||
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 gconv.Bool(v[2]) {
|
||||
m.Where(v[0], v[1])
|
||||
} else {
|
||||
m.WhereNot(gconv.String(v[0]), v[1])
|
||||
}
|
||||
}
|
||||
if len(v) == 2 {
|
||||
|
||||
@@ -42,15 +42,15 @@ const (
|
||||
maxMatrixSize = 227 // 矩阵维度(覆盖最大属性ID 226)
|
||||
)
|
||||
|
||||
// 合法单属性ID集合(快速校验)
|
||||
var validSingleElementIDs = map[int]bool{
|
||||
// 合法单属性ID集合(按ID直接索引,避免运行时 map 查找)
|
||||
var validSingleElementIDs = [maxMatrixSize]bool{
|
||||
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,
|
||||
221: true, 222: true, 223: true, 224: true, 225: true, 226: true,
|
||||
}
|
||||
|
||||
// 元素名称映射(全属性对应,便于日志输出)
|
||||
var elementNameMap = map[ElementType]string{
|
||||
// 元素名称映射(按ID直接索引,便于日志输出)
|
||||
var elementNameMap = [maxMatrixSize]string{
|
||||
ElementTypeGrass: "GRASS",
|
||||
ElementTypeWater: "WATER",
|
||||
ElementTypeFire: "FIRE",
|
||||
@@ -198,46 +198,55 @@ type ElementCombination struct {
|
||||
ID int // 组合唯一ID
|
||||
}
|
||||
|
||||
// 全局预加载资源(程序启动时init初始化,运行时直接使用)
|
||||
// 全局预加载资源(程序启动时初始化,运行时只读)
|
||||
var (
|
||||
// 元素组合池:key=组合ID,value=组合实例(预加载所有合法组合)
|
||||
elementCombinationPool = make(map[int]*ElementCombination, 150) // 128双+26单=154,预分配足够容量
|
||||
// 单属性克制矩阵(预初始化所有特殊克制关系,默认1.0)
|
||||
matrix [maxMatrixSize][maxMatrixSize]float64
|
||||
validCombinationIDs [maxMatrixSize]bool
|
||||
elementCombinationPool [maxMatrixSize]ElementCombination
|
||||
dualElementSecondaryPool [maxMatrixSize]ElementType
|
||||
matrix [maxMatrixSize][maxMatrixSize]float64
|
||||
Calculator *ElementCalculator
|
||||
)
|
||||
|
||||
// init 预加载所有资源(程序启动时执行一次,无并发问题)
|
||||
func init() {
|
||||
// 1. 初始化单属性克制矩阵
|
||||
initFullTableMatrix()
|
||||
initElementCombinationPool()
|
||||
Calculator = NewElementCalculator()
|
||||
}
|
||||
|
||||
// 2. 预加载所有单属性组合
|
||||
for id := range validSingleElementIDs {
|
||||
combo := &ElementCombination{
|
||||
Primary: ElementType(id),
|
||||
Secondary: nil,
|
||||
ID: id,
|
||||
func initElementCombinationPool() {
|
||||
for id, valid := range validSingleElementIDs {
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
validCombinationIDs[id] = true
|
||||
elementCombinationPool[id] = ElementCombination{
|
||||
Primary: ElementType(id),
|
||||
ID: id,
|
||||
}
|
||||
elementCombinationPool[id] = combo
|
||||
}
|
||||
|
||||
// 3. 预加载所有双属性组合
|
||||
for dualID, atts := range dualElementMap {
|
||||
primaryID, secondaryID := atts[0], atts[1]
|
||||
// 按ID升序排序,保证组合一致性
|
||||
primary, secondary := ElementType(primaryID), ElementType(secondaryID)
|
||||
if primary > secondary {
|
||||
primary, secondary = secondary, primary
|
||||
}
|
||||
combo := &ElementCombination{
|
||||
|
||||
dualElementSecondaryPool[dualID] = secondary
|
||||
validCombinationIDs[dualID] = true
|
||||
elementCombinationPool[dualID] = ElementCombination{
|
||||
Primary: primary,
|
||||
Secondary: &secondary,
|
||||
Secondary: &dualElementSecondaryPool[dualID],
|
||||
ID: dualID,
|
||||
}
|
||||
elementCombinationPool[dualID] = combo
|
||||
}
|
||||
}
|
||||
|
||||
func isValidCombinationID(id int) bool {
|
||||
return id > 0 && id < maxMatrixSize && validCombinationIDs[id]
|
||||
}
|
||||
|
||||
// IsDual 判断是否为双属性
|
||||
func (ec *ElementCombination) IsDual() bool {
|
||||
return ec.Secondary != nil
|
||||
@@ -245,84 +254,82 @@ func (ec *ElementCombination) IsDual() bool {
|
||||
|
||||
// Elements 获取所有属性列表
|
||||
func (ec *ElementCombination) Elements() []ElementType {
|
||||
if ec.IsDual() {
|
||||
return []ElementType{ec.Primary, *ec.Secondary}
|
||||
if secondary := ec.Secondary; secondary != nil {
|
||||
return []ElementType{ec.Primary, *secondary}
|
||||
}
|
||||
return []ElementType{ec.Primary}
|
||||
}
|
||||
|
||||
// String 友好格式化输出
|
||||
func (ec *ElementCombination) String() string {
|
||||
primaryName := elementNameMap[ec.Primary]
|
||||
if !ec.IsDual() {
|
||||
return fmt.Sprintf("(%s)", primaryName)
|
||||
if secondary := ec.Secondary; secondary != nil {
|
||||
return fmt.Sprintf("(%s, %s)", elementNameMap[ec.Primary], elementNameMap[*secondary])
|
||||
}
|
||||
return fmt.Sprintf("(%s, %s)", primaryName, elementNameMap[*ec.Secondary])
|
||||
return fmt.Sprintf("(%s)", elementNameMap[ec.Primary])
|
||||
}
|
||||
|
||||
// ElementCalculator 无锁元素克制计算器(依赖预加载资源)
|
||||
// ElementCalculator 无锁元素克制计算器(所有倍数在初始化阶段预计算)
|
||||
type ElementCalculator struct {
|
||||
offensiveCache map[string]float64 // 攻击克制缓存(运行时填充,无并发写)
|
||||
offensiveTable [maxMatrixSize][maxMatrixSize]float64
|
||||
}
|
||||
|
||||
// NewElementCalculator 创建计算器实例(仅初始化缓存)
|
||||
// NewElementCalculator 创建计算器实例(构建只读查表缓存)
|
||||
func NewElementCalculator() *ElementCalculator {
|
||||
return &ElementCalculator{
|
||||
offensiveCache: make(map[string]float64, 4096), // 预分配大容量缓存
|
||||
c := &ElementCalculator{}
|
||||
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)
|
||||
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) {
|
||||
combo, exists := elementCombinationPool[id]
|
||||
if !exists {
|
||||
if !isValidCombinationID(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) {
|
||||
// 1. 获取预加载的组合实例
|
||||
attacker, err := c.GetCombination(attackerID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("attacker invalid: %w", err)
|
||||
if !isValidCombinationID(attackerID) {
|
||||
return 0, fmt.Errorf("attacker invalid: invalid element combination ID: %d", attackerID)
|
||||
}
|
||||
defender, err := c.GetCombination(defenderID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("defender invalid: %w", err)
|
||||
if !isValidCombinationID(defenderID) {
|
||||
return 0, fmt.Errorf("defender invalid: invalid element combination ID: %d", defenderID)
|
||||
}
|
||||
|
||||
// 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
|
||||
return c.offensiveTable[attackerID][defenderID], nil
|
||||
}
|
||||
|
||||
// calculateMultiplier 核心克制计算逻辑
|
||||
func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombination) float64 {
|
||||
// 场景1:单→单
|
||||
if !attacker.IsDual() && !defender.IsDual() {
|
||||
return c.getMatrixValue(attacker.Primary, defender.Primary)
|
||||
}
|
||||
|
||||
// 场景2:单→双
|
||||
if !attacker.IsDual() {
|
||||
y1, y2 := defender.Primary, *defender.Secondary
|
||||
m1 := c.getMatrixValue(attacker.Primary, y1)
|
||||
m2 := c.getMatrixValue(attacker.Primary, y2)
|
||||
|
||||
switch {
|
||||
case m1 == 2 && m2 == 2:
|
||||
return 4.0
|
||||
@@ -333,12 +340,10 @@ func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombi
|
||||
}
|
||||
}
|
||||
|
||||
// 场景3:双→单
|
||||
if !defender.IsDual() {
|
||||
return c.calculateDualToSingle(attacker.Primary, *attacker.Secondary, defender.Primary)
|
||||
}
|
||||
|
||||
// 场景4:双→双
|
||||
x1, x2 := attacker.Primary, *attacker.Secondary
|
||||
y1, y2 := defender.Primary, *defender.Secondary
|
||||
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 {
|
||||
k1 := c.getMatrixValue(attacker1, defender)
|
||||
k2 := c.getMatrixValue(attacker2, defender)
|
||||
|
||||
switch {
|
||||
case k1 == 2 && k2 == 2:
|
||||
return 4.0
|
||||
@@ -361,60 +365,49 @@ func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender
|
||||
}
|
||||
}
|
||||
|
||||
var Calculator = NewElementCalculator()
|
||||
|
||||
// TestAllScenarios 全场景测试(验证预加载和计算逻辑)
|
||||
func TestAllScenarios() {
|
||||
|
||||
// 测试1:单→单(草→水)
|
||||
m1, _ := Calculator.GetOffensiveMultiplier(1, 2)
|
||||
fmt.Println("草→水: %.2f(预期2.0)", m1)
|
||||
if math.Abs(m1-2.0) > 0.001 {
|
||||
fmt.Println("测试1失败:实际%.2f", m1)
|
||||
}
|
||||
|
||||
// 测试2:特殊单→单(混沌→虚空)
|
||||
m2, _ := Calculator.GetOffensiveMultiplier(222, 226)
|
||||
fmt.Println("混沌→虚空: %.2f(预期0.0)", m2)
|
||||
if math.Abs(m2-0.0) > 0.001 {
|
||||
fmt.Println("测试2失败:实际%.2f", m2)
|
||||
}
|
||||
|
||||
// 测试3:单→双(火→冰龙(43))
|
||||
m3, _ := Calculator.GetOffensiveMultiplier(3, 43)
|
||||
fmt.Println("火→冰龙: %.2f(预期1.5)", m3)
|
||||
if math.Abs(m3-1.5) > 0.001 {
|
||||
fmt.Println("测试3失败:实际%.2f", m3)
|
||||
}
|
||||
|
||||
// 测试4:双→特殊单(混沌暗影(92)→神灵(223))
|
||||
m4, _ := Calculator.GetOffensiveMultiplier(92, 223)
|
||||
fmt.Println("混沌暗影→神灵: %.2f(预期1.25)", m4)
|
||||
if math.Abs(m4-1.25) > 0.001 {
|
||||
fmt.Println("测试4失败:实际%.2f", m4)
|
||||
}
|
||||
|
||||
// 测试5:双→双(虚空邪灵(113)→混沌远古(98))
|
||||
m5, _ := Calculator.GetOffensiveMultiplier(113, 98)
|
||||
fmt.Println("虚空邪灵→混沌远古: %.2f(预期0.875", m5)
|
||||
if math.Abs(m5-0.875) > 0.001 {
|
||||
fmt.Println("测试5失败:实际%.2f", m5)
|
||||
}
|
||||
|
||||
// 测试6:缓存命中
|
||||
m6, _ := Calculator.GetOffensiveMultiplier(113, 98)
|
||||
if math.Abs(m6-m5) > 0.001 {
|
||||
fmt.Println("测试6失败:缓存未命中")
|
||||
}
|
||||
|
||||
// 测试7:含无效组合(电→地面)
|
||||
m7, _ := Calculator.GetOffensiveMultiplier(5, 7)
|
||||
fmt.Println("电→地面: %.2f(预期0.0)", m7)
|
||||
if math.Abs(m7-0.0) > 0.001 {
|
||||
fmt.Println("测试7失败:实际%.2f", m7)
|
||||
}
|
||||
|
||||
// 测试8:双属性含无效(电战斗→地面)
|
||||
m8, _ := Calculator.GetOffensiveMultiplier(35, 7)
|
||||
fmt.Println("电战斗→地面: %.2f(预期0.25)", m8)
|
||||
if math.Abs(m8-0.25) > 0.001 {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package data
|
||||
|
||||
import "github.com/gogf/gf/v2/util/grand"
|
||||
|
||||
// 1. 质量枚举常量(保持不变)
|
||||
const (
|
||||
BitmapFilterQualityLow = 1 // LOW:应用1次滤镜
|
||||
@@ -20,6 +22,35 @@ const (
|
||||
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字段
|
||||
// 3. 核心结构体:BlurX/BlurY/Strength 改为 uint8
|
||||
type GlowFilter struct {
|
||||
@@ -53,6 +84,6 @@ type GlowFilter struct {
|
||||
// ItemInfo
|
||||
// 用于表示发放物品的信息
|
||||
type ItemInfo struct {
|
||||
ItemId uint32 `json:"itemId" description:"发放物品ID"` // 发放物品ID,
|
||||
ItemCnt uint32 `json:"itemCount" description:"发放物品的数量"` // 发放物品的数量,
|
||||
ItemId int64 `struc:"uint32"`
|
||||
ItemCnt int64 `struc:"uint32"`
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func (s *cacheStore[T]) Del(ctx context.Context, key string) error {
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ func newSessionStore() *cacheStore[uint32] {
|
||||
}
|
||||
|
||||
// newUserOnlineStore 创建用户在线状态缓存实例
|
||||
func newUserOnlineStore() *cacheStore[uint16] {
|
||||
return &cacheStore[uint16]{
|
||||
func newUserOnlineStore() *cacheStore[uint32] {
|
||||
return &cacheStore[uint32]{
|
||||
manager: cool.CacheManager,
|
||||
prefix: "blazing:useronline:",
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func newEmailCodeStore() *cacheStore[int] {
|
||||
// sessionManager 会话管理器
|
||||
type sessionManager struct {
|
||||
sessionStore *cacheStore[uint32] // 会话缓存
|
||||
userOnlineStore *cacheStore[uint16] // 用户在线状态缓存
|
||||
userOnlineStore *cacheStore[uint32] // 用户在线状态缓存
|
||||
emailCodeStore *cacheStore[int] // 邮件注册码缓存
|
||||
}
|
||||
|
||||
@@ -52,12 +52,12 @@ func newSessionManager() *sessionManager {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ type EffectArg struct {
|
||||
SideEffect []struct {
|
||||
ID int `json:"ID"`
|
||||
SideEffectArgcount int `json:"SideEffectArgcount"`
|
||||
SideEffectArg string `json:"SideEffectArg,omitempty"`
|
||||
SideEffectArg rawFlexibleString `json:"SideEffectArg,omitempty"`
|
||||
} `json:"SideEffect"`
|
||||
} `json:"SideEffects"`
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
_ "blazing/common/data/xmlres/packed"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"fmt"
|
||||
|
||||
"github.com/ECUST-XX/xml"
|
||||
"github.com/gogf/gf/v2/os/gres"
|
||||
@@ -14,22 +14,36 @@ import (
|
||||
|
||||
var path string
|
||||
|
||||
func readConfigContent(path string) []byte {
|
||||
return gres.GetContent(path)
|
||||
}
|
||||
|
||||
func getXml[T any](path string) T {
|
||||
|
||||
// 解析XML到结构体
|
||||
var xmls T
|
||||
|
||||
t1 := gres.GetContent(path)
|
||||
t1 := readConfigContent(path)
|
||||
xml.Unmarshal(t1, &xmls)
|
||||
|
||||
return xmls
|
||||
}
|
||||
func getJson[T any](path string) T {
|
||||
|
||||
// 解析XML到结构体
|
||||
// 解析JSON到结构体
|
||||
var xmls T
|
||||
t1 := gres.GetContent(path)
|
||||
json.Unmarshal(t1, &xmls)
|
||||
t1 := readConfigContent(path)
|
||||
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
|
||||
}
|
||||
@@ -40,7 +54,7 @@ var (
|
||||
// EffectArgsConfig EffectArg //arg参数
|
||||
//TalkConfig TalkRoot //任务配置
|
||||
// //Monster MonsterRoot //野怪配置
|
||||
MonsterMap map[int]TMapConfig
|
||||
//MonsterMap map[int]TMapConfig
|
||||
//Skill MovesTbl //技能配置
|
||||
SkillMap map[int]Move
|
||||
PetMAP map[int]PetInfo //宠物配置
|
||||
@@ -58,8 +72,6 @@ var (
|
||||
|
||||
func Initfile() {
|
||||
//gres.Dump()
|
||||
path1, _ := os.Getwd()
|
||||
path = path1 + "/public/config/"
|
||||
path = "config/"
|
||||
MapConfig = getXml[Maps](path + "210.xml")
|
||||
|
||||
@@ -78,19 +90,19 @@ func Initfile() {
|
||||
})
|
||||
//TalkConfig = getXml[TalkRoot](path + "talk.xml")
|
||||
|
||||
MonsterMap = utils.ToMap(getXml[MonsterRoot](path+"地图配置野怪.xml").Maps, func(m TMapConfig) int {
|
||||
return m.ID
|
||||
// MonsterMap = utils.ToMap(getXml[MonsterRoot](path+"地图配置野怪.xml").Maps, func(m TMapConfig) int {
|
||||
// return m.ID
|
||||
|
||||
})
|
||||
// })
|
||||
|
||||
ShopMap = utils.ToMap(getXml[ShopRoot](path+"地图配置野怪.xml").Items, func(m ShopItem) int {
|
||||
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))
|
||||
for _, v := range Skill.Moves {
|
||||
SkillMap = make(map[int]Move, len(skillConfig.MovesTbl.Moves.Move))
|
||||
for _, v := range skillConfig.MovesTbl.Moves.Move {
|
||||
v.SideEffectS = ParseSideEffectArgs(v.SideEffect)
|
||||
v.SideEffectArgS = ParseSideEffectArgs(v.SideEffectArg)
|
||||
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
|
||||
|
||||
})
|
||||
|
||||
@@ -11,23 +11,24 @@ type Items struct {
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
ID int `xml:"ID,attr"` // 物品ID(与items.xml一致)
|
||||
Name string `xml:"Name,attr"` // 物品名称
|
||||
Rarity int `xml:"Rarity,attr,omitempty"` // 稀有度
|
||||
ItemType int `xml:"ItemType,attr"` // 物品类型(0:胶囊 1:体力药剂 2:活力药剂)
|
||||
Max int `xml:"Max,attr"` // 最大堆叠数量
|
||||
Price int `xml:"Price,attr"` // 价格
|
||||
Bonus float64 `xml:"Bonus,attr,omitempty"` // 倍率(如捕捉胶囊的加成倍数,修正为浮点型)
|
||||
Tradability int `xml:"Tradability,attr"` // 可交易性(0/1)
|
||||
VipTradability int `xml:"VipTradability,attr"` // VIP可交易性(0/1)
|
||||
DailyKey int `xml:"DailyKey,attr,omitempty"` // 每日限制键值
|
||||
DailyOutMax int `xml:"DailyOutMax,attr,omitempty"` // 每日最大产出
|
||||
Wd int `xml:"wd,attr"` // 未知属性
|
||||
UseMax int `xml:"UseMax,attr"` // 最大使用次数
|
||||
LifeTime int `xml:"LifeTime,attr"` // 生命周期(0为永久)
|
||||
Purpose int `xml:"purpose,attr"` // 用途标识
|
||||
Bean int `xml:"Bean,attr,omitempty"` // 豆子数量
|
||||
Hide int `xml:"Hide,attr"` // 是否隐藏(0/1)
|
||||
ID int `xml:"ID,attr"` // 物品ID(与items.xml一致)
|
||||
Name string `xml:"Name,attr"` // 物品名称
|
||||
Rarity int `xml:"Rarity,attr,omitempty"` // 稀有度
|
||||
ItemType int `xml:"ItemType,attr"` // 物品类型(0:胶囊 1:体力药剂 2:活力药剂)
|
||||
Max int `xml:"Max,attr"` // 最大堆叠数量
|
||||
Price int `xml:"Price,attr"` // 价格
|
||||
Bonus float64 `xml:"Bonus,attr,omitempty"` // 倍率(如捕捉胶囊的加成倍数,修正为浮点型)
|
||||
Tradability int `xml:"Tradability,attr"` // 可交易性(0/1)
|
||||
VipTradability int `xml:"VipTradability,attr"` // VIP可交易性(0/1)
|
||||
DailyKey int `xml:"DailyKey,attr,omitempty"` // 每日限制键值
|
||||
DailyOutMax int `xml:"DailyOutMax,attr,omitempty"` // 每日最大产出
|
||||
Wd int `xml:"wd,attr"` // 未知属性
|
||||
UseMax int `xml:"UseMax,attr"` // 最大使用次数
|
||||
LifeTime int `xml:"LifeTime,attr"` // 生命周期(0为永久)
|
||||
Purpose int `xml:"purpose,attr"` // 用途标识
|
||||
Bean int `xml:"Bean,attr,omitempty"` // 豆子数量
|
||||
Hide int `xml:"Hide,attr"` // 是否隐藏(0/1)
|
||||
Texture int `xml:"Texture,attr,omitempty"`
|
||||
Sort int `xml:"Sort,attr,omitempty"` // 排序序号
|
||||
Des string `xml:"des,attr,omitempty"` // 物品用途(XML中无该属性,保留字段供自定义)
|
||||
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
|
||||
|
||||
import "github.com/ECUST-XX/xml"
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ECUST-XX/xml"
|
||||
)
|
||||
|
||||
// Move 表示怪物可学习的技能
|
||||
type PetMoves struct {
|
||||
@@ -15,28 +20,28 @@ type LearnableMoves struct {
|
||||
|
||||
// PetInfo 表示一个怪物的信息
|
||||
type PetInfo struct {
|
||||
ID int `xml:"ID,attr"`
|
||||
DefName string `xml:"DefName,attr"` // 名字
|
||||
Type int `xml:"Type,attr"` // 类型
|
||||
IsLarge int `xml:"IsLarge,attr"` // 是否为大型怪物
|
||||
GrowthType int `xml:"GrowthType,attr"` // 成长类型
|
||||
HP int `xml:"HP,attr"` // 血量种族值
|
||||
Atk uint32 `xml:"Atk,attr"` // 攻击种族值
|
||||
Def uint32 `xml:"Def,attr"` // 防御种族值
|
||||
SpAtk uint32 `xml:"SpAtk,attr"` // 特殊攻击种族值
|
||||
SpDef uint32 `xml:"SpDef,attr"` // 特殊防御种族值
|
||||
Spd uint32 `xml:"Spd,attr"` // 速度种族值
|
||||
YieldingExp int `xml:"YieldingExp,attr"` // 击败后获得的经验值
|
||||
CatchRate int `xml:"CatchRate,attr"` // 捕捉率
|
||||
YieldingEV string `xml:"YieldingEV,attr"` // 努力值奖励,格式为"HP Atk Def SpAtk SpDef Spd"
|
||||
EvolvesFrom int `xml:"EvolvesFrom,attr"` // 进化前的怪物ID
|
||||
EvolvesTo uint32 `xml:"EvolvesTo,attr"` // 进化后的怪物ID
|
||||
EvolvFlag int `xml:"EvolvFlag,attr"` //<!-- EvolvFlag: 0 - 直接进化(等级到了就进化); 1~49 - 触发进化,默认值: 0 (默认直接进化) -->
|
||||
EvolvingLv int `xml:"EvolvingLv,attr"` // 进化等级
|
||||
FreeForbidden int `xml:"FreeForbidden,attr"` // 是否禁止放生
|
||||
FuseMaster int `xml:"FuseMaster,attr"` // 是否可作为融合主素材
|
||||
FuseSub int `xml:"FuseSub,attr"` // 是否可作为融合副素材
|
||||
Gender int `xml:"Gender,attr"` // 性别 0-无性别 1-雄性 2-雌性
|
||||
ID int `xml:"ID,attr"`
|
||||
DefName string `xml:"DefName,attr"` // 名字
|
||||
Type int `xml:"Type,attr"` // 类型
|
||||
IsLarge int `xml:"IsLarge,attr"` // 是否为大型怪物
|
||||
GrowthType int `xml:"GrowthType,attr"` // 成长类型
|
||||
HP int `xml:"HP,attr"` // 血量种族值
|
||||
Atk uint32 `xml:"Atk,attr"` // 攻击种族值
|
||||
Def uint32 `xml:"Def,attr"` // 防御种族值
|
||||
SpAtk uint32 `xml:"SpAtk,attr"` // 特殊攻击种族值
|
||||
SpDef uint32 `xml:"SpDef,attr"` // 特殊防御种族值
|
||||
Spd uint32 `xml:"Spd,attr"` // 速度种族值
|
||||
YieldingExp int `xml:"YieldingExp,attr"` // 击败后获得的经验值
|
||||
CatchRate int `xml:"CatchRate,attr"` // 捕捉率
|
||||
YieldingEV string `xml:"YieldingEV,attr"` // 努力值奖励,格式为"HP Atk Def SpAtk SpDef Spd"
|
||||
EvolvesFrom int `xml:"EvolvesFrom,attr"` // 进化前的怪物ID
|
||||
EvolvesTo uint32 `xml:"EvolvesTo,attr"` // 进化后的怪物ID
|
||||
EvolvFlag int `xml:"EvolvFlag,attr"` //<!-- EvolvFlag: 0 - 直接进化(等级到了就进化); 1~49 - 触发进化,默认值: 0 (默认直接进化) -->
|
||||
EvolvingLv int `xml:"EvolvingLv,attr"` // 进化等级
|
||||
FreeForbidden int `xml:"FreeForbidden,attr"` // 是否禁止放生
|
||||
FuseMaster int `xml:"FuseMaster,attr"` // 是否可作为融合主素材
|
||||
FuseSub int `xml:"FuseSub,attr"` // 是否可作为融合副素材
|
||||
// Gender int `xml:"Gender,attr"` // 性别 0-无性别 1-雄性 2-雌性
|
||||
PetClass int `xml:"PetClass,attr"` // 宠物类别
|
||||
FormParam float64 `xml:"FormParam,attr"` // 形态参数
|
||||
CharacterAttrParam int `xml:"CharacterAttrParam,attr"` // 特性参数
|
||||
@@ -45,6 +50,7 @@ type PetInfo struct {
|
||||
Recycle int `xml:"Recycle,attr"` // 是否可回收
|
||||
LearnableMoves LearnableMoves `xml:"LearnableMoves"` // 可学习的技能
|
||||
NaturalEnemy string `xml:"NaturalEnemy,attr"` //天敌
|
||||
YieldingEVValues []int64 `xml:"-"` // 预解析后的努力值奖励
|
||||
}
|
||||
|
||||
func (basic *PetInfo) GetBasic() uint32 {
|
||||
@@ -61,3 +67,16 @@ type Monsters struct {
|
||||
XMLName xml.Name `xml:"Monsters"`
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -33,51 +34,156 @@ type MovesTbl struct {
|
||||
Moves []Move `xml:"Moves>Move"`
|
||||
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 {
|
||||
XMLName xml.Name `xml:"MovesTbl"`
|
||||
Moves map[int]Move
|
||||
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 定义单个技能的结构
|
||||
type Move struct {
|
||||
ID int `xml:"ID,attr"`
|
||||
Name string `xml:"Name,attr"`
|
||||
ID int `xml:"ID,attr" json:"ID"`
|
||||
Name string `xml:"Name,attr" json:"Name"`
|
||||
|
||||
Category int `xml:"Category,attr"` //属性
|
||||
Type int `xml:"Type,attr"` //类型
|
||||
Power int `xml:"Power,attr"` //威力
|
||||
MaxPP int `xml:"MaxPP,attr"` //最大PP
|
||||
Accuracy int `xml:"Accuracy,attr"` //命中率
|
||||
CritRate int `xml:"CritRate,attr,omitempty"` //暴击率
|
||||
Priority int `xml:"Priority,attr,omitempty"` //优先级
|
||||
MustHit int `xml:"MustHit,attr,omitempty"` //是否必中
|
||||
SwapElemType int `xml:"SwapElemType,attr,omitempty"` //技能交换属性
|
||||
CopyElemType int `xml:"CopyElemType,attr,omitempty"` // 技能复制属性
|
||||
CritAtkFirst int `xml:"CritAtkFirst,attr,omitempty"` // 先出手时必定致命一击
|
||||
CritAtkSecond int `xml:"CritAtkSecond,attr,omitempty"` //后出手时必定致命一击
|
||||
CritSelfHalfHp int `xml:"CritSelfHalfHp,attr,omitempty"` //自身体力低于一半时必定致命一击
|
||||
CritFoeHalfHp int `xml:"CritFoeHalfHp,attr,omitempty"` //对方体力低于一半时必定致命一击
|
||||
DmgBindLv int `xml:"DmgBindLv,attr,omitempty"` //使对方受到的伤害值等于自身的等级
|
||||
PwrBindDv int `xml:"PwrBindDv,attr,omitempty"` //威力(power)取决于自身的潜力(个体值)
|
||||
PwrDouble int `xml:"PwrDouble,attr,omitempty"` //攻击时,若对方处于异常状态, 则威力翻倍;
|
||||
DmgBindHpDv int `xml:"DmgBindHpDv,attr,omitempty"` //使对方受到的伤害值等于自身的体力值
|
||||
SideEffect string `xml:"SideEffect,attr,omitempty"`
|
||||
SideEffectArg string `xml:"SideEffectArg,attr,omitempty"`
|
||||
Category int `xml:"Category,attr" json:"Category"` //属性
|
||||
Type int `xml:"Type,attr" json:"Type"` //类型
|
||||
Power int `xml:"Power,attr" json:"Power"` //威力
|
||||
MaxPP int `xml:"MaxPP,attr" json:"MaxPP"` //最大PP
|
||||
Accuracy int `xml:"Accuracy,attr" json:"Accuracy"` //命中率
|
||||
CritRate int `xml:"CritRate,attr,omitempty" json:"CritRate,omitempty"` //暴击率
|
||||
Priority int `xml:"Priority,attr,omitempty" json:"Priority,omitempty"` //优先级
|
||||
MustHit int `xml:"MustHit,attr,omitempty" json:"MustHit,omitempty"` //是否必中
|
||||
SwapElemType int `xml:"SwapElemType,attr,omitempty" json:"SwapElemType,omitempty"` //技能交换属性
|
||||
CopyElemType int `xml:"CopyElemType,attr,omitempty" json:"CopyElemType,omitempty"` // 技能复制属性
|
||||
CritAtkFirst int `xml:"CritAtkFirst,attr,omitempty" json:"CritAtkFirst,omitempty"` // 先出手时必定致命一击
|
||||
CritAtkSecond int `xml:"CritAtkSecond,attr,omitempty" json:"CritAtkSecond,omitempty"` //后出手时必定致命一击
|
||||
CritSelfHalfHp int `xml:"CritSelfHalfHp,attr,omitempty" json:"CritSelfHalfHp,omitempty"` //自身体力低于一半时必定致命一击
|
||||
CritFoeHalfHp int `xml:"CritFoeHalfHp,attr,omitempty" json:"CritFoeHalfHp,omitempty"` //对方体力低于一半时必定致命一击
|
||||
DmgBindLv int `xml:"DmgBindLv,attr,omitempty" json:"DmgBindLv,omitempty"` //使对方受到的伤害值等于自身的等级
|
||||
PwrBindDv int `xml:"PwrBindDv,attr,omitempty" json:"PwrBindDv,omitempty"` //威力(power)取决于自身的潜力(个体值)
|
||||
PwrDouble int `xml:"PwrDouble,attr,omitempty" json:"PwrDouble,omitempty"` //攻击时,若对方处于异常状态, 则威力翻倍;
|
||||
DmgBindHpDv int `xml:"DmgBindHpDv,attr,omitempty" json:"DmgBindHpDv,omitempty"` //使对方受到的伤害值等于自身的体力值
|
||||
SideEffect string `xml:"SideEffect,attr,omitempty" json:"SideEffect,omitempty"`
|
||||
SideEffectArg string `xml:"SideEffectArg,attr,omitempty" json:"SideEffectArg,omitempty"`
|
||||
SideEffectS []int
|
||||
SideEffectArgS []int
|
||||
AtkNum int `xml:"AtkNum,attr,omitempty"`
|
||||
Url string `xml:"Url,attr,omitempty"`
|
||||
AtkNum int `xml:"AtkNum,attr,omitempty" json:"AtkNum,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 {
|
||||
ID int `xml:"ID,attr"`
|
||||
Help string `xml:"help,attr"`
|
||||
Des string `xml:"des,attr"`
|
||||
ID int `xml:"ID,attr" json:"ID"`
|
||||
Help string `xml:"help,attr" json:"help"`
|
||||
Des string `xml:"des,attr" json:"des"`
|
||||
}
|
||||
|
||||
// ReadHTTPFile 通过HTTP GET请求获取远程文件内容
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module blazing/common
|
||||
|
||||
go 1.23.0
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/panjf2000/gnet v1.6.7
|
||||
|
||||
@@ -33,23 +33,10 @@ func GetServerInfoList(isdebug int32) []ServerInfo {
|
||||
|
||||
}
|
||||
tt.Name = v.Name
|
||||
tt.Port = v.Port
|
||||
tt.Port =uint16( v.Port)
|
||||
ret1 = append(ret1, *tt)
|
||||
}
|
||||
|
||||
//t, ok := cool.GetClient(v.Port)
|
||||
|
||||
// if ok {
|
||||
// cool.Logger.Info(context.TODO(), "服务器假踢人")
|
||||
// err := t.KickPerson(0) //实现指定服务器踢人
|
||||
|
||||
// if err == nil {
|
||||
// // tt.Friends = v.Friends
|
||||
// ret1 = append(ret1, *tt)
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
array.Sort(&ret1, func(a ServerInfo, b ServerInfo) bool {
|
||||
@@ -102,7 +89,7 @@ type ServerInfo struct {
|
||||
// 服务器IP, 16字节UTF-8, 不足16补齐到16
|
||||
IP string `struc:"[16]byte"` // 定长模式:16字节
|
||||
// 端口
|
||||
Port uint16
|
||||
Port uint16
|
||||
// 好友在线的个数
|
||||
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,124 +1,166 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"blazing/common/data/share"
|
||||
"blazing/cool"
|
||||
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
config "blazing/modules/config/service"
|
||||
|
||||
"github.com/filecoin-project/go-jsonrpc"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// Define the server handler
|
||||
type ServerHandler struct{}
|
||||
|
||||
// 实现踢人
|
||||
func (*ServerHandler) Kick(_ context.Context, userid uint32) error {
|
||||
|
||||
cool.Logger.Info(context.TODO(), "服务器收到踢人")
|
||||
useid1, err := share.ShareManager.GetUserOnline(userid)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found", err)
|
||||
}
|
||||
|
||||
cl, ok := cool.GetClient(useid1)
|
||||
if ok {
|
||||
err := cl.KickPerson(userid) //实现指定服务器踢人
|
||||
if err != nil {
|
||||
return fmt.Errorf("踢人失败", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 注册logic服务器
|
||||
func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint16) error {
|
||||
cool.Logger.Debug(context.Background(), "注册logic服务器", id, port)
|
||||
|
||||
//TODO 待修复滚动更新可能导致的玩家可以同时在旧服务器和新服务器同时在线的bug
|
||||
revClient, ok := jsonrpc.ExtractReverseClient[cool.ClientHandler](ctx)
|
||||
if !ok {
|
||||
return fmt.Errorf("no reverse client")
|
||||
}
|
||||
t := config.NewServerService().GetServerID((id))
|
||||
|
||||
aa, ok := cool.GetClient(t.Port)
|
||||
if ok && aa != nil { //如果已经存在且这个端口已经被存过
|
||||
aa.QuitSelf(0)
|
||||
}
|
||||
cool.AddClient(port, &revClient)
|
||||
|
||||
//Refurh()
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// func StartServer() {
|
||||
// // create a new server instance
|
||||
// rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler](""))
|
||||
|
||||
// rpcServer.Register("", &ServerHandler{})
|
||||
// cool.Logger.Debug(context.Background(), "jsonrpc server start", rpcport)
|
||||
// // go time.AfterFunc(3000, func() {
|
||||
// // testjsonrpc()
|
||||
// // })
|
||||
|
||||
// err := http.ListenAndServe("0.0.0.0:"+rpcport, rpcServer)
|
||||
// cool.Logger.Debug(context.Background(), "jsonrpc server fail", err)
|
||||
// }
|
||||
func CServer() *jsonrpc.RPCServer {
|
||||
// create a new server instance
|
||||
rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler](""))
|
||||
|
||||
rpcServer.Register("", &ServerHandler{})
|
||||
|
||||
return rpcServer
|
||||
// err := http.ListenAndServe("0.0.0.0:"+rpcport, rpcServer)
|
||||
// // cool.Logger.Debug(context.Background(), "jsonrpc server fail", err)
|
||||
}
|
||||
|
||||
var closer jsonrpc.ClientCloser
|
||||
|
||||
func StartClient(id, port uint16, callback any) *struct {
|
||||
Kick func(uint32) error
|
||||
|
||||
RegisterLogic func(uint16, uint16) error
|
||||
} {
|
||||
|
||||
var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc"
|
||||
//rpcaddr = "127.0.0.1"
|
||||
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(uint16, uint16) error
|
||||
|
||||
// UserLogin func(int32, int32) error //用户登录事件
|
||||
// UserLogout func(int32, int32) error //用户登出事件
|
||||
}
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"blazing/common/data/share"
|
||||
"blazing/cool"
|
||||
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
config "blazing/modules/config/service"
|
||||
|
||||
"github.com/filecoin-project/go-jsonrpc"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// Define the server handler
|
||||
type ServerHandler struct{}
|
||||
|
||||
const kickForwardTimeout = 3 * time.Second
|
||||
|
||||
// 实现踢人
|
||||
func (*ServerHandler) Kick(_ context.Context, userid uint32) error {
|
||||
useid1, err := share.ShareManager.GetUserOnline(userid)
|
||||
if err != nil || useid1 == 0 {
|
||||
// 请求到达时用户已离线,直接视为成功
|
||||
return nil
|
||||
}
|
||||
|
||||
cl, ok := cool.GetClientOnly(useid1)
|
||||
if !ok || cl == nil {
|
||||
// 目标服务器不在线,清理僵尸在线标记并视为成功
|
||||
_ = share.ShareManager.DeleteUserOnline(userid)
|
||||
cool.DeleteClientOnly(useid1)
|
||||
return nil
|
||||
}
|
||||
|
||||
resultCh := make(chan error, 1)
|
||||
go func() {
|
||||
resultCh <- cl.KickPerson(userid) // 实现指定服务器踢人
|
||||
}()
|
||||
|
||||
select {
|
||||
case callErr := <-resultCh:
|
||||
if callErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 调用失败后兜底:用户若已离线/切服/目标服不在线都算成功
|
||||
useid2, err2 := share.ShareManager.GetUserOnline(userid)
|
||||
if err2 != nil || useid2 == 0 || useid2 != useid1 {
|
||||
return nil
|
||||
}
|
||||
if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil {
|
||||
_ = share.ShareManager.DeleteUserOnline(userid)
|
||||
cool.DeleteClientOnly(useid2)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 仍在线则返回失败,不按成功处理
|
||||
return callErr
|
||||
case <-time.After(kickForwardTimeout):
|
||||
// 仅防止无限等待;超时不算成功
|
||||
useid2, err2 := share.ShareManager.GetUserOnline(userid)
|
||||
if err2 != nil || useid2 == 0 || useid2 != useid1 {
|
||||
return nil
|
||||
}
|
||||
if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil {
|
||||
_ = share.ShareManager.DeleteUserOnline(userid)
|
||||
cool.DeleteClientOnly(useid2)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("kick timeout, user still online: uid=%d server=%d", userid, useid2)
|
||||
}
|
||||
}
|
||||
|
||||
// 注册logic服务器
|
||||
func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error {
|
||||
fmt.Println("注册logic服务器", id, port)
|
||||
|
||||
//TODO 待修复滚动更新可能导致的玩家可以同时在旧服务器和新服务器同时在线的bug
|
||||
revClient, ok := jsonrpc.ExtractReverseClient[cool.ClientHandler](ctx)
|
||||
if !ok {
|
||||
return fmt.Errorf("no reverse client")
|
||||
}
|
||||
t := config.NewServerService().GetServerID((id))
|
||||
|
||||
aa, ok := cool.GetClient(t.OnlineID, t.Port)
|
||||
if ok && aa != nil { //如果已经存在且这个端口已经被存过
|
||||
aa.QuitSelf(0)
|
||||
}
|
||||
cool.AddClient(100000*id+port, &revClient)
|
||||
|
||||
//Refurh()
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (*ServerHandler) MatchJoinOrUpdate(_ context.Context, payload PVPMatchJoinPayload) error {
|
||||
return DefaultPVPMatchCoordinator().JoinOrUpdate(payload)
|
||||
}
|
||||
|
||||
func (*ServerHandler) MatchCancel(_ context.Context, userID uint32) error {
|
||||
DefaultPVPMatchCoordinator().Cancel(userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
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,224 +1,266 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"blazing/common/socket/codec"
|
||||
"blazing/cool"
|
||||
"blazing/logic/service/player"
|
||||
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"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,
|
||||
gnet.WithMulticore(true),
|
||||
gnet.WithTicker(true),
|
||||
|
||||
// gnet.WithReusePort(true),
|
||||
// gnet.WithReuseAddr(true),
|
||||
gnet.WithSocketRecvBuffer(s.bufferSize))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
_ = s.eng.Stop(context.Background())
|
||||
s.workerPool.Release()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
||||
// 1. 打印错误信息
|
||||
|
||||
fmt.Println(context.TODO(), "panic 错误:", err)
|
||||
|
||||
}
|
||||
}()
|
||||
// 识别 RST 导致的连接中断(错误信息含 "connection reset")
|
||||
// if err != nil && (strings.Contains(err.Error(), "connection reset") || strings.Contains(err.Error(), "reset by peer")) {
|
||||
// remoteIP := c.RemoteAddr().(*net.TCPAddr).IP.String()
|
||||
|
||||
// log.Printf("RST 攻击检测: 来源 %s, 累计攻击次数 %d", remoteIP)
|
||||
|
||||
// // 防护逻辑:临时封禁异常 IP(可扩展为 IP 黑名单)
|
||||
// // go s.tempBlockIP(remoteIP, 5*time.Minute)
|
||||
// }
|
||||
//fmt.Println(err, c.RemoteAddr().String(), "断开连接")
|
||||
atomic.AddInt64(&s.connected, -1)
|
||||
|
||||
//logging.Infof("conn[%v] disconnected", c.RemoteAddr().String())
|
||||
v, _ := c.Context().(*player.ClientData)
|
||||
v.LF.Close()
|
||||
if v.Player != nil {
|
||||
v.SaveL.Do(func() { //使用保存锁,确保在踢人和掉线的时候只保存一次
|
||||
//cool.Loger.Info(context.TODO(), "准备保存", v.Player.Info.UserID)
|
||||
v.Player.Save() //保存玩家数据
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
//}
|
||||
//关闭连接
|
||||
return
|
||||
}
|
||||
func (s *Server) OnTick() (delay time.Duration, action gnet.Action) {
|
||||
glog.Info(context.Background(), gtime.Now().ISO8601(), cool.Config.ServerInfo.OnlineID, "链接数", atomic.LoadInt64(&s.connected))
|
||||
if s.quit && atomic.LoadInt64(&s.connected) == 0 {
|
||||
//执行正常退出逻辑
|
||||
os.Exit(0)
|
||||
}
|
||||
return 30 * time.Second, gnet.None
|
||||
}
|
||||
func (s *Server) OnBoot(eng gnet.Engine) gnet.Action {
|
||||
s.eng = eng
|
||||
|
||||
// cool.Loger.Infof(context.Background(), " server is listening on %s\n", s.addr)
|
||||
|
||||
return gnet.None
|
||||
}
|
||||
|
||||
func (s *Server) OnOpen(conn gnet.Conn) (out []byte, action gnet.Action) {
|
||||
if s.network != "tcp" {
|
||||
return nil, gnet.Close
|
||||
}
|
||||
|
||||
if conn.Context() == nil {
|
||||
conn.SetContext(player.NewClientData(conn)) //注入data
|
||||
}
|
||||
|
||||
atomic.AddInt64(&s.connected, 1)
|
||||
|
||||
return nil, gnet.None
|
||||
}
|
||||
|
||||
func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
||||
// 1. 打印错误信息
|
||||
|
||||
fmt.Println(context.TODO(), "panic 错误:", err)
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
ws := c.Context().(*player.ClientData).Wsmsg
|
||||
if ws.Tcp { //升级失败时候防止缓冲区溢出
|
||||
return s.handleTCP(c)
|
||||
|
||||
}
|
||||
|
||||
tt, len1 := ws.ReadBufferBytes(c)
|
||||
if tt == gnet.Close {
|
||||
|
||||
return gnet.Close
|
||||
}
|
||||
|
||||
ok, action := ws.Upgrade(c)
|
||||
if action != gnet.None { //连接断开
|
||||
return action
|
||||
}
|
||||
if !ok { //升级失败,说明是tcp连接
|
||||
ws.Tcp = true
|
||||
|
||||
return s.handleTCP(c)
|
||||
|
||||
}
|
||||
// fmt.Println(ws.Buf.Bytes())
|
||||
c.Discard(len1)
|
||||
|
||||
messages, err := ws.Decode(c)
|
||||
if err != nil {
|
||||
return gnet.Close
|
||||
}
|
||||
if messages == nil {
|
||||
return
|
||||
}
|
||||
|
||||
t := c.Context().(*player.ClientData)
|
||||
//client := conn.RemoteAddr().String()
|
||||
s.workerPool.Submit(func() { //TODO 这里可能存在顺序执行问题,待修复
|
||||
for _, msg := range messages {
|
||||
t.LF.Producer().Write(msg.Payload)
|
||||
//t.OnEvent(msg.Payload)
|
||||
}
|
||||
})
|
||||
|
||||
return gnet.None
|
||||
}
|
||||
|
||||
func (s *Server) handleTCP(conn gnet.Conn) (action gnet.Action) {
|
||||
|
||||
conn.Context().(*player.ClientData).IsCrossDomain.Do(func() { //跨域检测
|
||||
handle(conn)
|
||||
})
|
||||
|
||||
data, err := s.codec.Decode(conn)
|
||||
if err != nil {
|
||||
if err == codec.ErrIncompletePacket {
|
||||
|
||||
return
|
||||
}
|
||||
return gnet.Close
|
||||
|
||||
}
|
||||
|
||||
s.workerPool.Submit(func() { //TODO 这里可能存在顺序执行问题,待修复
|
||||
//conn.Context().(*player.ClientData).OnEvent(data)
|
||||
if t, ok := conn.Context().(*player.ClientData); ok {
|
||||
t.LF.Producer().Write(data)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if conn.InboundBuffered() > 0 {
|
||||
if err := conn.Wake(nil); err != nil { // wake up the connection manually to avoid missing the leftover data
|
||||
cool.Logger.Errorf(context.Background(), "failed to wake up the connection, %v", err)
|
||||
return gnet.Close
|
||||
}
|
||||
}
|
||||
return action
|
||||
|
||||
}
|
||||
|
||||
// CROSS_DOMAIN 定义跨域策略文件内容
|
||||
const CROSS_DOMAIN = "<?xml version=\"1.0\"?><!DOCTYPE cross-domain-policy><cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>\x00"
|
||||
|
||||
// TEXT 定义跨域请求的文本格式
|
||||
const TEXT = "<policy-file-request/>\x00"
|
||||
|
||||
func handle(c gnet.Conn) {
|
||||
|
||||
// 读取数据并检查是否为跨域请求
|
||||
data, err := c.Peek(len(TEXT))
|
||||
if err != nil {
|
||||
log.Printf("Error reading cross-domain request: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if string(data) == TEXT { //判断是否是跨域请求
|
||||
log.Printf("Received cross-domain request from %s", c.RemoteAddr())
|
||||
// 处理跨域请求
|
||||
c.Write([]byte(CROSS_DOMAIN))
|
||||
c.Discard(len(TEXT))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//return
|
||||
}
|
||||
package socket
|
||||
|
||||
import (
|
||||
"blazing/common/socket/codec"
|
||||
"blazing/cool"
|
||||
"blazing/logic/service/player"
|
||||
"blazing/modules/config/service"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/panjf2000/gnet/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
minPacketLen = 17
|
||||
maxPacketLen = 10 * 1024
|
||||
)
|
||||
|
||||
func (s *Server) Boot(serverid, port uint32) error {
|
||||
// go s.bootws()
|
||||
s.serverid = serverid
|
||||
s.port = port
|
||||
|
||||
err := gnet.Run(s, s.network+"://"+s.addr,
|
||||
gnet.WithMulticore(true),
|
||||
gnet.WithTicker(true),
|
||||
|
||||
// 其他调优配置↓
|
||||
gnet.WithTCPNoDelay(gnet.TCPNoDelay), // 禁用Nagle算法(降低延迟,适合小数据包场景)
|
||||
//gnet.WithReusePort(true), // 开启SO_REUSEPORT(多核下提升并发)
|
||||
//gnet.WithReadBufferCap(1024*64), // 读缓冲区64KB(根据业务调整,默认太小)
|
||||
// gnet.WithWriteBufferCap(1024*64), // 写缓冲区64KB
|
||||
|
||||
//gnet.WithLockOSThread(true), // 绑定goroutine到OS线程(减少上下文切换)
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
_ = s.eng.Stop(context.Background())
|
||||
s.workerPool.Release()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
||||
if t, ok := c.Context().(*player.ClientData); ok {
|
||||
if t.Player != nil {
|
||||
if t.Player.Info != nil {
|
||||
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
|
||||
go t.Player.SaveOnDisconnect()
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
atomic.AddInt64(&cool.Connected, -1)
|
||||
|
||||
v, _ := c.Context().(*player.ClientData)
|
||||
if v != nil {
|
||||
v.Close()
|
||||
if v.Player != nil {
|
||||
v.Player.Save() //保存玩家数据
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
os.Exit(0)
|
||||
}
|
||||
return 30 * time.Second, gnet.None
|
||||
}
|
||||
|
||||
func (s *Server) OnBoot(eng gnet.Engine) gnet.Action {
|
||||
s.eng = eng
|
||||
service.NewServerService().SetServerID(s.serverid, s.port)
|
||||
return gnet.None
|
||||
}
|
||||
|
||||
func (s *Server) OnOpen(conn gnet.Conn) (out []byte, action gnet.Action) {
|
||||
if s.network != "tcp" {
|
||||
return nil, gnet.Close
|
||||
}
|
||||
if conn.Context() == nil {
|
||||
conn.SetContext(player.NewClientData(conn))
|
||||
}
|
||||
atomic.AddInt64(&cool.Connected, 1)
|
||||
return nil, gnet.None
|
||||
}
|
||||
|
||||
func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if t, ok := c.Context().(*player.ClientData); ok {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
client := c.Context().(*player.ClientData)
|
||||
if s.discorse && !client.IsCrossDomainChecked() {
|
||||
handled, ready, action := handle(c)
|
||||
if action != gnet.None {
|
||||
return action
|
||||
}
|
||||
if handled {
|
||||
client.MarkCrossDomainChecked()
|
||||
return gnet.None
|
||||
}
|
||||
if !ready {
|
||||
return gnet.None
|
||||
}
|
||||
client.MarkCrossDomainChecked()
|
||||
}
|
||||
|
||||
ws := client.Wsmsg
|
||||
if ws.Tcp {
|
||||
return s.handleTCP(c)
|
||||
}
|
||||
|
||||
readAction, inboundLen := ws.ReadBufferBytes(c)
|
||||
if readAction == gnet.Close {
|
||||
return gnet.Close
|
||||
}
|
||||
|
||||
state, action := ws.Upgrade(c)
|
||||
if action != gnet.None {
|
||||
return action
|
||||
}
|
||||
if state == player.UpgradeNeedMoreData {
|
||||
return gnet.None
|
||||
}
|
||||
if state == player.UpgradeUseTCP {
|
||||
return s.handleTCP(c)
|
||||
}
|
||||
|
||||
if inboundLen > 0 {
|
||||
if _, err := c.Discard(inboundLen); err != nil {
|
||||
return gnet.Close
|
||||
}
|
||||
ws.ResetInboundMirror()
|
||||
}
|
||||
|
||||
messages, err := ws.Decode(c)
|
||||
if err != nil {
|
||||
return gnet.Close
|
||||
}
|
||||
if messages == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
if !s.onevent(c, msg.Payload) {
|
||||
return gnet.Close
|
||||
}
|
||||
}
|
||||
return gnet.None
|
||||
}
|
||||
|
||||
func (s *Server) handleTCP(conn gnet.Conn) (action gnet.Action) {
|
||||
client := conn.Context().(*player.ClientData)
|
||||
if s.discorse && !client.IsCrossDomainChecked() {
|
||||
handled, ready, action := handle(conn)
|
||||
if action != gnet.None {
|
||||
return action
|
||||
}
|
||||
if !ready {
|
||||
return gnet.None
|
||||
}
|
||||
if handled {
|
||||
client.MarkCrossDomainChecked()
|
||||
return gnet.None
|
||||
}
|
||||
client.MarkCrossDomainChecked()
|
||||
}
|
||||
|
||||
body, err := s.codec.Decode(conn)
|
||||
if err != nil {
|
||||
if errors.Is(err, codec.ErrIncompletePacket) {
|
||||
return gnet.None
|
||||
}
|
||||
return gnet.Close
|
||||
}
|
||||
if !s.onevent(conn, body) {
|
||||
return gnet.Close
|
||||
}
|
||||
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 {
|
||||
gnet.BuiltinEventEngine
|
||||
eng gnet.Engine
|
||||
addr string
|
||||
connected int64
|
||||
eng gnet.Engine
|
||||
addr string
|
||||
|
||||
network string
|
||||
multicore bool
|
||||
bufferSize int
|
||||
@@ -24,7 +24,9 @@ type Server struct {
|
||||
handler Handler
|
||||
discorse bool
|
||||
quit bool
|
||||
batchRead int
|
||||
// batchRead int
|
||||
serverid uint32
|
||||
port uint32
|
||||
}
|
||||
|
||||
type Option func(*Server)
|
||||
@@ -36,9 +38,9 @@ func NewServer(options ...Option) *Server {
|
||||
// handler: handler.NewTomeeHandler(), //请求返回
|
||||
codec: codec.NewTomeeSocketCodec(), //默认解码器 len+pack
|
||||
workerPool: goroutine.Default(),
|
||||
bufferSize: 4096, //默认缓冲区大小
|
||||
bufferSize: 40960, //默认缓冲区大小
|
||||
multicore: true,
|
||||
batchRead: 8,
|
||||
//batchRead: 8,
|
||||
//discorse: true,
|
||||
}
|
||||
for _, option := range options {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"blazing/common/socket/errorcode"
|
||||
"blazing/cool"
|
||||
"blazing/logic/service/common"
|
||||
"blazing/logic/service/player"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -15,20 +12,22 @@ type Broadcast struct {
|
||||
}
|
||||
|
||||
func (s *Server) Broadcast(t string) int {
|
||||
cool.Logger.Info(context.TODO(), "全服广播", t)
|
||||
var count int
|
||||
player.Mainplayer.Range(func(key uint32, value *player.Player) bool {
|
||||
count++
|
||||
value.SendPackCmd(50003, &Broadcast{
|
||||
|
||||
player.Mainplayer.Range(func(key uint32, value *player.ClientData) bool {
|
||||
|
||||
value.Player.SendPackCmd(50003, &Broadcast{
|
||||
Name: t,
|
||||
})
|
||||
return false
|
||||
return true
|
||||
})
|
||||
|
||||
return count
|
||||
return player.Mainplayer.Count()
|
||||
}
|
||||
|
||||
const kickTimeout = 5 * time.Second
|
||||
|
||||
func (s *Server) KickPerson(a int) error {
|
||||
cool.Logger.Info(context.TODO(), "检测到踢人请求", a)
|
||||
|
||||
if a == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -43,26 +42,32 @@ func (s *Server) QuitSelf(a int) error {
|
||||
|
||||
s.quit = true
|
||||
if a != 0 {
|
||||
player.Mainplayer.Range(func(key uint32, value *player.Player) bool {
|
||||
value.Kick(1)
|
||||
player.Mainplayer.Range(func(key uint32, value *player.ClientData) bool {
|
||||
if value != nil {
|
||||
value.Player.Kick(true)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
|
||||
go func() {
|
||||
player.Mainplayer.Range(func(key uint32, value *player.Player) bool {
|
||||
head := common.NewTomeeHeader(1001, value.Info.UserID)
|
||||
|
||||
head.Result = uint32(errorcode.ErrorCodes.ErrXinPlanSleepMode)
|
||||
|
||||
value.SendPack(head.Pack(nil))
|
||||
player.Mainplayer.Range(func(key uint32, value *player.ClientData) bool {
|
||||
if value != nil {
|
||||
value.Player.KickMessage()
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
<-time.After(10 * time.Minute)
|
||||
player.Mainplayer.Range(func(key uint32, value *player.Player) bool {
|
||||
value.Kick(1)
|
||||
player.Mainplayer.Range(func(key uint32, value *player.ClientData) bool {
|
||||
if value != nil {
|
||||
value.Player.Kick(true)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,13 @@ package bitset32
|
||||
import "math/bits"
|
||||
|
||||
func popcntSlice(s []uint32) uint64 {
|
||||
|
||||
// int r = 0;
|
||||
// while(n)
|
||||
// {
|
||||
// n &= (n - 1);
|
||||
// ++r;
|
||||
// }
|
||||
var cnt int
|
||||
for _, x := range s {
|
||||
cnt += bits.OnesCount32(x)
|
||||
|
||||
@@ -1,221 +1,195 @@
|
||||
package csmap
|
||||
|
||||
import (
|
||||
"blazing/cool"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"github.com/mhmtszr/concurrent-swiss-map/maphash"
|
||||
|
||||
"github.com/mhmtszr/concurrent-swiss-map/swiss"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// CsMap 基于官方 sync.Map 重构,完全兼容原有接口
|
||||
type CsMap[K comparable, V any] struct {
|
||||
hasher func(key K) uint64
|
||||
shards []shard[K, V]
|
||||
shardCount uint64
|
||||
size uint64
|
||||
inner sync.Map // 核心替换为官方 sync.Map
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
// New function creates *CsMap[K, V].
|
||||
// New 创建基于 sync.Map 的并发安全 Map,兼容原有配置参数(参数无实际作用)
|
||||
func New[K comparable, V any](options ...OptFunc[K, V]) *CsMap[K, V] {
|
||||
m := CsMap[K, V]{
|
||||
hasher: maphash.NewHasher[K]().Hash,
|
||||
shardCount: 32,
|
||||
}
|
||||
m := &CsMap[K, V]{}
|
||||
// 遍历配置项(兼容原有代码,无实际逻辑)
|
||||
for _, option := range options {
|
||||
option(&m)
|
||||
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
|
||||
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]) {
|
||||
return func(csMap *CsMap[K, V]) {
|
||||
csMap.shardCount = count
|
||||
}
|
||||
return 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]) {
|
||||
csMap.hasher = h
|
||||
}
|
||||
return func(csMap *CsMap[K, V]) {}
|
||||
}
|
||||
|
||||
func WithSize[K comparable, V any](size uint64) func(csMap *CsMap[K, V]) {
|
||||
return func(csMap *CsMap[K, V]) {
|
||||
csMap.size = size
|
||||
}
|
||||
return func(csMap *CsMap[K, V]) {}
|
||||
}
|
||||
|
||||
func (m *CsMap[K, V]) getShard(key K) HashShardPair[K, V] {
|
||||
u := m.hasher(key)
|
||||
return HashShardPair[K, V]{
|
||||
hash: u,
|
||||
shard: m.shards[u%m.shardCount],
|
||||
}
|
||||
}
|
||||
// -------------------------- 核心操作方法(基于 sync.Map 实现) --------------------------
|
||||
|
||||
// Store 存储键值对,兼容原有接口
|
||||
func (m *CsMap[K, V]) Store(key K, value V) {
|
||||
hashShardPair := m.getShard(key)
|
||||
shard := hashShardPair.shard
|
||||
shard.Lock()
|
||||
shard.items.PutWithHash(key, value, hashShardPair.hash)
|
||||
shard.Unlock()
|
||||
m.inner.Store(key, value)
|
||||
}
|
||||
func (m *CsMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
|
||||
T, OK := m.inner.LoadOrStore(key, value)
|
||||
return T.(V), OK
|
||||
}
|
||||
|
||||
// Delete 删除指定键,返回是否删除成功
|
||||
func (m *CsMap[K, V]) Delete(key K) bool {
|
||||
hashShardPair := m.getShard(key)
|
||||
shard := hashShardPair.shard
|
||||
shard.Lock()
|
||||
defer shard.Unlock()
|
||||
return shard.items.DeleteWithHash(key, hashShardPair.hash)
|
||||
// sync.Map.Delete 无返回值,需先 Load 判断是否存在
|
||||
_, ok := m.inner.Load(key)
|
||||
if ok {
|
||||
m.inner.Delete(key)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// DeleteIf 满足条件时删除
|
||||
func (m *CsMap[K, V]) DeleteIf(key K, condition func(value V) bool) bool {
|
||||
hashShardPair := m.getShard(key)
|
||||
shard := hashShardPair.shard
|
||||
shard.Lock()
|
||||
defer shard.Unlock()
|
||||
value, ok := shard.items.GetWithHash(key, hashShardPair.hash)
|
||||
if ok && condition(value) {
|
||||
return shard.items.DeleteWithHash(key, hashShardPair.hash)
|
||||
// 先 Load 获取值,再判断条件
|
||||
val, ok := m.inner.Load(key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
v, okCast := val.(V)
|
||||
if !okCast {
|
||||
return false
|
||||
}
|
||||
|
||||
if condition(v) {
|
||||
m.inner.Delete(key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Load 获取指定键的值
|
||||
func (m *CsMap[K, V]) Load(key K) (V, bool) {
|
||||
hashShardPair := m.getShard(key)
|
||||
shard := hashShardPair.shard
|
||||
shard.RLock()
|
||||
defer shard.RUnlock()
|
||||
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()
|
||||
var zero V
|
||||
val, ok := m.inner.Load(key)
|
||||
if !ok {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// 类型断言(保证类型安全)
|
||||
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 {
|
||||
count := 0
|
||||
for i := range m.shards {
|
||||
shard := m.shards[i]
|
||||
shard.RLock()
|
||||
count += shard.items.Count()
|
||||
shard.RUnlock()
|
||||
}
|
||||
m.inner.Range(func(key, value any) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
// SetIfAbsent 仅当键不存在时设置值
|
||||
func (m *CsMap[K, V]) SetIfAbsent(key K, value V) {
|
||||
hashShardPair := m.getShard(key)
|
||||
shard := hashShardPair.shard
|
||||
shard.Lock()
|
||||
_, ok := shard.items.GetWithHash(key, hashShardPair.hash)
|
||||
if !ok {
|
||||
shard.items.PutWithHash(key, value, hashShardPair.hash)
|
||||
}
|
||||
shard.Unlock()
|
||||
m.inner.LoadOrStore(key, value)
|
||||
}
|
||||
|
||||
func (m *CsMap[K, V]) SetIf(key K, conditionFn func(previousVale V, previousFound bool) (value V, set bool)) {
|
||||
hashShardPair := m.getShard(key)
|
||||
shard := hashShardPair.shard
|
||||
shard.Lock()
|
||||
value, found := shard.items.GetWithHash(key, hashShardPair.hash)
|
||||
value, ok := conditionFn(value, found)
|
||||
if ok {
|
||||
shard.items.PutWithHash(key, value, hashShardPair.hash)
|
||||
// SetIf 根据条件设置值
|
||||
func (m *CsMap[K, V]) SetIf(key K, conditionFn func(previousValue V, previousFound bool) (value V, set bool)) {
|
||||
prevVal, found := m.inner.Load(key)
|
||||
var prevV V
|
||||
if found {
|
||||
prevV, _ = prevVal.(V)
|
||||
}
|
||||
|
||||
// 执行条件函数
|
||||
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) {
|
||||
hashShardPair := m.getShard(key)
|
||||
shard := hashShardPair.shard
|
||||
shard.Lock()
|
||||
_, ok := shard.items.GetWithHash(key, hashShardPair.hash)
|
||||
if ok {
|
||||
shard.items.PutWithHash(key, value, hashShardPair.hash)
|
||||
// 先判断是否存在,再设置
|
||||
if _, ok := m.inner.Load(key); ok {
|
||||
m.inner.Store(key, value)
|
||||
}
|
||||
shard.Unlock()
|
||||
}
|
||||
|
||||
// IsEmpty 判断是否为空
|
||||
func (m *CsMap[K, V]) IsEmpty() bool {
|
||||
return m.Count() == 0
|
||||
}
|
||||
|
||||
// Tuple 保留原有结构体(兼容序列化逻辑)
|
||||
type Tuple[K comparable, V any] struct {
|
||||
Key K
|
||||
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)) {
|
||||
ch := make(chan Tuple[K, V], m.Count())
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var stopFlag atomic.Bool
|
||||
|
||||
listenCompleted := m.listen(f, ch)
|
||||
m.produce(ctx, ch)
|
||||
listenCompleted.Wait()
|
||||
// 基于 sync.Map 的 Range 实现,无额外 goroutine/channel
|
||||
m.inner.Range(func(key, value any) bool {
|
||||
// 检测终止标志
|
||||
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) {
|
||||
tmp := make(map[K]V, m.Count())
|
||||
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 {
|
||||
tmp := make(map[K]V, m.Count())
|
||||
|
||||
tmp := make(map[K]V)
|
||||
if err := json.Unmarshal(b, &tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清空原有数据
|
||||
m.Clear()
|
||||
// 批量存储
|
||||
for key, val := range tmp {
|
||||
m.Store(key, val)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CsMap[K, V]) produce(ctx context.Context, ch chan Tuple[K, V]) {
|
||||
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(), "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(), "panic 错误:", err)
|
||||
|
||||
}
|
||||
}()
|
||||
for t := range ch {
|
||||
if stop := f(t.Key, t.Val); stop {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &wg
|
||||
}
|
||||
// -------------------------- 移除所有无用的旧方法(produce/listen 等) --------------------------
|
||||
|
||||
@@ -2,33 +2,4 @@ module github.com/zmexing/go-sensitive-word
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
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
|
||||
)
|
||||
require github.com/orcaman/concurrent-map/v2 v2.0.1
|
||||
|
||||
@@ -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/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 (
|
||||
"bufio"
|
||||
"errors"
|
||||
"github.com/imroc/req/v3"
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
)
|
||||
|
||||
// MemoryModel 使用并发 map 实现的内存词库
|
||||
@@ -62,26 +64,40 @@ func (m *MemoryModel) LoadDictEmbed(contents ...string) error {
|
||||
}
|
||||
|
||||
// 从远程 HTTP 地址加载词库
|
||||
// LoadDictHttp 批量从 HTTP 地址加载字典(标准库 net/http 实现)
|
||||
func (m *MemoryModel) LoadDictHttp(urls ...string) error {
|
||||
// 【标准库】创建带超时的客户端,防止请求卡死
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second, // 超时控制,非常重要
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
err := func(url string) error {
|
||||
httpRes, err := req.Get(url)
|
||||
// 立即执行函数,解决 defer 循环变量问题
|
||||
err := func(u string) error {
|
||||
// 标准库 GET 请求
|
||||
resp, err := client.Get(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if httpRes == nil {
|
||||
return errors.New("nil http response")
|
||||
}
|
||||
if httpRes.StatusCode != http.StatusOK {
|
||||
return errors.New(httpRes.GetStatus())
|
||||
return fmt.Errorf("请求失败 %s: %w", u, err)
|
||||
}
|
||||
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
}(httpRes.Body)
|
||||
// 必须 defer 关闭 body,防止资源泄漏(标准库固定写法)
|
||||
defer func() {
|
||||
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)
|
||||
|
||||
// 任意一个失败,立即返回
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,5 +1,63 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"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/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) {
|
||||
for i := range slice {
|
||||
if predicate(slice[i]) {
|
||||
@@ -24,3 +82,129 @@ func RemoveLast(s string) string {
|
||||
runes := []rune(s)
|
||||
return string(runes[:len(runes)-1])
|
||||
}
|
||||
|
||||
// RandomByWeight 根据整数权重随机选择元素(优化后的泛型版本)
|
||||
// 入参:
|
||||
// - elements: 待随机的元素集合(非空)
|
||||
// - weights: 对应元素的权重(非负整数,长度需与elements一致;长度不匹配/总权重为0时降级为等概率随机)
|
||||
//
|
||||
// 返回:
|
||||
// - 随机选中的元素
|
||||
// - 错误(仅当elements为空/权重值为负时返回)
|
||||
func RandomByWeight[Element any, Weight integer](elements []Element, weights []Weight) (Element, error) {
|
||||
// 定义泛型零值,用于错误返回
|
||||
var zero Element
|
||||
|
||||
// 1. 核心合法性校验:元素集合不能为空
|
||||
if len(elements) == 0 {
|
||||
return zero, errors.New("elements set is empty (cannot random from empty slice)")
|
||||
}
|
||||
|
||||
// 2. 权重数组合法性校验:长度不匹配/为空时,降级为等概率随机(兼容原逻辑)
|
||||
elemLen := len(elements)
|
||||
if len(weights) == 0 || len(weights) != elemLen {
|
||||
return elements[rand.IntN(elemLen)], nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
intWeights[i] = intW
|
||||
totalWeight += intW
|
||||
}
|
||||
|
||||
// 4. 总权重为0时,降级为等概率随机
|
||||
if totalWeight == 0 {
|
||||
return elements[rand.IntN(elemLen)], nil
|
||||
}
|
||||
|
||||
// 5. 计算前缀和(权重随机核心逻辑,复用预转换的int64权重)
|
||||
prefixWeights := make([]int64, elemLen)
|
||||
prefixWeights[0] = intWeights[0]
|
||||
for i := 1; i < elemLen; i++ {
|
||||
prefixWeights[i] = prefixWeights[i-1] + intWeights[i]
|
||||
}
|
||||
|
||||
// 6. 生成随机数并匹配前缀和(用int64避免溢出)
|
||||
randomValue := grand.Intn(int(totalWeight))
|
||||
for i, sum := range prefixWeights {
|
||||
if randomValue < int(sum) {
|
||||
return elements[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
// 极端兜底:理论上不会走到这里(randomValue < totalWeight),返回第一个元素保证不panic
|
||||
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型概率,泛型)**************************
|
||||
// randomByProbs 兼容原有入参格式:接收任意类型元素数组 + string型概率数组
|
||||
// 内部完成 string[] -> int[] 的转换,然后调用核心函数 randomByIntProbs
|
||||
func RandomByProbs[T any](natureSet []T, probs []string) (T, error) {
|
||||
//var zeroT T
|
||||
|
||||
// 1. 若string概率数组为空,直接调用核心函数(核心函数会处理降级逻辑)
|
||||
if len(probs) == 0 {
|
||||
return RandomByWeight(natureSet, []int{})
|
||||
}
|
||||
|
||||
// 2. string[] 转换为 int[](使用 gconv.Int 完成转换)
|
||||
probInts := make([]int, len(probs))
|
||||
for i, pStr := range probs {
|
||||
// gconv.Int 灵活转换,失败返回0
|
||||
probInts[i] = gconv.Int(pStr)
|
||||
}
|
||||
|
||||
// 3. 调用核心函数,复用概率计算逻辑
|
||||
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) {
|
||||
for i, value := range *di {
|
||||
p[i] = value
|
||||
}
|
||||
copy(p, (*di)[:])
|
||||
|
||||
return 2, nil
|
||||
}
|
||||
@@ -131,9 +129,7 @@ type SliceUInt8Struct struct {
|
||||
}
|
||||
|
||||
func (ia *SliceUInt8) Pack(p []byte, opt *Options) (int, error) {
|
||||
for i, value := range *ia {
|
||||
p[i] = value
|
||||
}
|
||||
copy(p, *ia)
|
||||
|
||||
return len(*ia) + 1, nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Field struct {
|
||||
@@ -76,40 +77,60 @@ func (f *Field) Size(val reflect.Value, options *Options) int {
|
||||
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
|
||||
if options.Order != nil {
|
||||
if options != nil && options.Order != nil {
|
||||
order = options.Order
|
||||
}
|
||||
if f.Ptr {
|
||||
val = val.Elem()
|
||||
if order == nil {
|
||||
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)
|
||||
kind := val.Kind()
|
||||
|
||||
// 4. 扁平化分支逻辑,减少嵌套
|
||||
switch typ {
|
||||
case Struct:
|
||||
return f.Fields.Pack(buf, val, options)
|
||||
|
||||
case Bool, Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64:
|
||||
size = typ.Size()
|
||||
if len(buf) < size {
|
||||
return 0, fmt.Errorf("buf size %d < required %d", len(buf), size)
|
||||
}
|
||||
|
||||
var n uint64
|
||||
switch f.kind {
|
||||
switch kind {
|
||||
case reflect.Bool:
|
||||
if val.Bool() {
|
||||
n = 1
|
||||
} else {
|
||||
n = 0
|
||||
}
|
||||
n = boolToUint64(val.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
n = uint64(val.Int())
|
||||
default:
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
n = val.Uint()
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported kind %s for numeric type %s", kind, typ)
|
||||
}
|
||||
|
||||
// 扁平化数值写入逻辑
|
||||
switch typ {
|
||||
case Bool:
|
||||
if n != 0 {
|
||||
buf[0] = 1
|
||||
} else {
|
||||
buf[0] = 0
|
||||
}
|
||||
buf[0] = byte(n)
|
||||
case Int8, Uint8:
|
||||
buf[0] = byte(n)
|
||||
case Int16, Uint16:
|
||||
@@ -117,33 +138,90 @@ func (f *Field) packVal(buf []byte, val reflect.Value, length int, options *Opti
|
||||
case Int32, Uint32:
|
||||
order.PutUint32(buf, uint32(n))
|
||||
case Int64, Uint64:
|
||||
order.PutUint64(buf, uint64(n))
|
||||
order.PutUint64(buf, n)
|
||||
}
|
||||
|
||||
case Float32, Float64:
|
||||
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()
|
||||
|
||||
switch typ {
|
||||
case Float32:
|
||||
order.PutUint32(buf, math.Float32bits(float32(n)))
|
||||
case Float64:
|
||||
order.PutUint64(buf, math.Float64bits(n))
|
||||
}
|
||||
|
||||
case String:
|
||||
switch f.kind {
|
||||
// 优化String类型:减少内存拷贝
|
||||
switch kind {
|
||||
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()
|
||||
copy(buf, []byte(val.String()))
|
||||
default:
|
||||
// TODO: handle kind != bytes here
|
||||
size = val.Len()
|
||||
if len(buf) < size {
|
||||
return 0, fmt.Errorf("buf size %d < bytes length %d", len(buf), size)
|
||||
}
|
||||
// 直接拷贝字节切片,避免冗余操作
|
||||
copy(buf, val.Bytes())
|
||||
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported kind %s for String type", kind)
|
||||
}
|
||||
|
||||
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:
|
||||
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) {
|
||||
@@ -174,7 +252,6 @@ func (f *Field) Pack(buf []byte, val reflect.Value, length int, options *Options
|
||||
copy(buf, buf[:length])
|
||||
return length, nil
|
||||
}
|
||||
return val.Len(), nil
|
||||
}
|
||||
pos := 0
|
||||
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
|
||||
if options.Order != nil {
|
||||
order = options.Order
|
||||
|
||||
@@ -2,6 +2,7 @@ package struc
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
@@ -103,76 +104,188 @@ func (f Fields) Pack(buf []byte, val reflect.Value, options *Options) (int, erro
|
||||
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 {
|
||||
// 解引用指针,直到拿到非指针类型的值
|
||||
for val.Kind() == reflect.Ptr {
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
v := val.Field(i)
|
||||
length := field.Len
|
||||
|
||||
// 获取当前字段的反射值
|
||||
fieldVal := val.Field(fieldIdx)
|
||||
// 获取字段长度(优先从Sizefrom读取,否则用默认Len)
|
||||
fieldLen := field.Len
|
||||
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.Slice {
|
||||
vals := v
|
||||
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
|
||||
}
|
||||
if err := f.unpackStructField(r, fieldVal, fieldLen, field, options); err != nil {
|
||||
return fmt.Errorf("unpack struct field index %d: %w", fieldIdx, err)
|
||||
}
|
||||
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 {
|
||||
typ := field.Type.Resolve(options)
|
||||
if typ == CustomType {
|
||||
if err := v.Addr().Interface().(Custom).Unpack(r, length, options); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
size := length * field.Type.Resolve(options).Size()
|
||||
if size < 8 {
|
||||
buf = tmp[:size]
|
||||
} 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
|
||||
}
|
||||
// 原逻辑:这里是OOM的核心触发点,现在已经提前做了长度校验
|
||||
sliceVal = reflect.MakeSlice(fieldVal.Type(), length, length)
|
||||
}
|
||||
|
||||
// 遍历切片/数组的每个元素,解析结构体
|
||||
for elemIdx := 0; elemIdx < length; elemIdx++ {
|
||||
elemVal := sliceVal.Index(elemIdx)
|
||||
if err := f.unpackSingleStructElem(r, elemVal, options); err != nil {
|
||||
return fmt.Errorf("slice elem %d: %w", elemIdx, 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
|
||||
}
|
||||
|
||||
// -------------------------- 抽离的辅助方法:处理基础类型字段 --------------------------
|
||||
// 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"
|
||||
"io"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
@@ -33,31 +34,88 @@ func init() {
|
||||
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) {
|
||||
// 1. 提前判空
|
||||
if data == nil {
|
||||
return reflect.Value{}, nil, fmt.Errorf("Invalid reflect.Value for nil")
|
||||
}
|
||||
|
||||
// 2. 初始反射值处理(和原逻辑一致)
|
||||
value := reflect.ValueOf(data)
|
||||
for value.Kind() == reflect.Ptr {
|
||||
next := value.Elem().Kind()
|
||||
elemValue := value
|
||||
for elemValue.Kind() == reflect.Ptr {
|
||||
next := elemValue.Elem().Kind()
|
||||
if next == reflect.Struct || next == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
elemValue = elemValue.Elem()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
switch value.Kind() {
|
||||
|
||||
// 3. 构建缓存键的基础类型(取解引用后的类型)
|
||||
baseType := elemValue.Type()
|
||||
var packer Packer
|
||||
var err error
|
||||
|
||||
// 4. 按类型分支处理,优先查缓存
|
||||
switch elemValue.Kind() {
|
||||
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:
|
||||
if !value.IsValid() {
|
||||
// 非结构体类型:检查有效性
|
||||
if !elemValue.IsValid() {
|
||||
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 {
|
||||
return PackWithOptions(w, data, nil)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/apcera/termtables"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
)
|
||||
|
||||
@@ -37,6 +38,12 @@ type Map struct {
|
||||
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) {
|
||||
// 示例XML数据
|
||||
xmlData := `<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
@@ -84,3 +84,18 @@ func RandomSlice[T any](slice []T, n int) []T {
|
||||
|
||||
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 (
|
||||
./common
|
||||
@@ -20,6 +20,7 @@ use (
|
||||
./common/utils/sturc
|
||||
./common/utils/timer
|
||||
./common/utils/xml
|
||||
./common/utils/zset
|
||||
./logic
|
||||
./login
|
||||
./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
|
||||
);
|
||||
49
help/修改自增id.sql
Normal file
49
help/修改自增id.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
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();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
15
help/异常经验数据修复.sql
Normal file
15
help/异常经验数据修复.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
SELECT
|
||||
player_id,
|
||||
-- 使用 CASE WHEN 实现条件判断:超过 1000 万重置为 200 万,否则保留原数值
|
||||
CASE
|
||||
WHEN (data->>'exp_pool')::bigint > 10000000 -- 判断是否超过 1000 万
|
||||
THEN 2000000::bigint -- 超过则重置为 200 万,统一为 bigint 类型避免类型不一致
|
||||
ELSE (data->>'exp_pool')::bigint -- 未超过则保留原 bigint 数值
|
||||
END AS online_time
|
||||
FROM "player_info"
|
||||
-- 保留 jsonb_exists 函数判断字段存在,避免占位符冲突
|
||||
WHERE
|
||||
jsonb_exists(data, 'exp_pool')
|
||||
AND (data->>'exp_pool')::bigint > 0 -- 此处仍保留原判断,过滤大于 0 的数据
|
||||
-- 按修正后的 online_time 降序排序
|
||||
ORDER BY online_time DESC;
|
||||
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
|
||||
|
||||
import (
|
||||
"blazing/common/rpc"
|
||||
"blazing/cool"
|
||||
"blazing/logic/service/common"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"strings"
|
||||
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"github.com/lunixbochs/struc"
|
||||
"github.com/panjf2000/gnet/v2"
|
||||
)
|
||||
|
||||
// Maincontroller 是控制器层共享变量。
|
||||
var Maincontroller = &Controller{} //注入service
|
||||
|
||||
// Controller 分发cmd逻辑实现
|
||||
type Controller struct {
|
||||
Port uint16
|
||||
RPCClient struct {
|
||||
UID uint32
|
||||
RPCClient *struct {
|
||||
Kick func(uint32) error
|
||||
|
||||
RegisterLogic func(uint16, uint16) error
|
||||
RegisterLogic func(uint32, uint32) error
|
||||
|
||||
MatchJoinOrUpdate func(rpc.PVPMatchJoinPayload) error
|
||||
|
||||
MatchCancel func(uint32) error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,154 +44,198 @@ type Controller struct {
|
||||
func ParseCmd[T any](data []byte) T {
|
||||
var result T
|
||||
// 使用struc.Unpack将字节数据解包到result变量中
|
||||
struc.Unpack(bytes.NewBuffer(data), &result)
|
||||
struc.Unpack(bytes.NewReader(data), &result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Init 初始化控制器,注册所有cmd处理方法
|
||||
// 参数 isGame: 标识是否为游戏服务器(true)或登录服务器(false)
|
||||
func Init(isGame bool) {
|
||||
// 获取控制器实例的反射值
|
||||
controllerValue := reflect.ValueOf(Maincontroller)
|
||||
|
||||
// 获取控制器类型
|
||||
controllerType := controllerValue.Type()
|
||||
|
||||
// 遍历控制器的所有方法
|
||||
for i := 0; i < controllerType.NumMethod(); i++ {
|
||||
method := controllerType.Method(i)
|
||||
methodValue := controllerValue.MethodByName(method.Name)
|
||||
methodValue := controllerValue.Method(i)
|
||||
methodType := methodValue.Type()
|
||||
|
||||
// 获取方法第一个参数的类型(请求结构体)
|
||||
if methodValue.Type().NumIn() == 0 {
|
||||
if methodType.NumIn() == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析请求结构体中的cmd标签
|
||||
for _, cmd := range getCmd(methodValue.Type().In(0)) {
|
||||
if cmd == 0 { // 说明不是有效的注册方法
|
||||
reqArgType := methodType.In(0)
|
||||
if reqArgType.Kind() != reflect.Ptr || reqArgType.Elem().Kind() != reflect.Struct {
|
||||
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, "跳过注册")
|
||||
continue
|
||||
}
|
||||
|
||||
// 根据服务器类型过滤cmd
|
||||
// 登录服务器只处理小于1000的cmd
|
||||
if methodType.NumIn() != 2 {
|
||||
glog.Warning(context.Background(), "方法参数数量必须为2", method.Name, "跳过注册")
|
||||
continue
|
||||
}
|
||||
|
||||
if !isGame && cmd > 1000 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 游戏服务器只处理大于等于1000的cmd
|
||||
if isGame && cmd < 1000 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 注册命令处理函数
|
||||
glog.Debug(context.Background(), "注册方法", cmd, method.Name)
|
||||
cmdInfo := cool.Cmd{
|
||||
Func: methodValue,
|
||||
Req: methodValue.Type().In(0).Elem(),
|
||||
// Res: , // TODO 待实现对不同用户初始化方法以取消全局cmdcache
|
||||
if cool.Config.ServerInfo.IsDebug != 0 {
|
||||
fmt.Println("注册方法", cmd, method.Name)
|
||||
}
|
||||
|
||||
reqTypeForNew := reqType
|
||||
cmdInfo := cool.Cmd{
|
||||
Func: methodValue,
|
||||
Req: reqType,
|
||||
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
|
||||
// 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
|
||||
|
||||
// getCmd 从结构体类型中提取绑定的cmd指令(递归查找嵌套结构体,支持值/指针类型的TomeeHeader)
|
||||
// 参数 typ: 待解析的结构体类型(支持多层指针)
|
||||
// 返回值: 解析到的cmd切片,无匹配/解析失败时返回[defaultCmdValue]
|
||||
func getCmd(typ reflect.Type) []uint32 {
|
||||
// 递归解引用所有指针类型(处理 *struct、**struct 等场景)
|
||||
type cmdBinding struct {
|
||||
cmds []uint32
|
||||
headerFieldIndex []int
|
||||
}
|
||||
|
||||
func normalizeStructType(typ reflect.Type) reflect.Type {
|
||||
for typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
// 非结构体类型直接返回默认值
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return []uint32{defaultCmdValue}
|
||||
// getCmdBinding 从结构体类型中提取绑定的cmd指令和头字段位置。
|
||||
func getCmdBinding(typ reflect.Type) cmdBinding {
|
||||
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++ {
|
||||
field := typ.Field(i)
|
||||
|
||||
// 尝试解析当前字段的cmd标签
|
||||
cmdSlice, err := parseCmdTagWithStructField(field)
|
||||
if err == nil { // 解析成功,直接返回结果
|
||||
return cmdSlice
|
||||
cmdSlice, isHeader, err := parseCmdTagWithStructField(field)
|
||||
if isHeader && err == nil {
|
||||
return cmdBinding{
|
||||
cmds: cmdSlice,
|
||||
headerFieldIndex: append([]int(nil), field.Index...),
|
||||
}, true
|
||||
}
|
||||
|
||||
// 递归处理嵌套结构体(值/指针类型)
|
||||
nestedTyp := field.Type
|
||||
if nestedTyp.Kind() == reflect.Ptr {
|
||||
nestedTyp = nestedTyp.Elem()
|
||||
nestedTyp := normalizeStructType(field.Type)
|
||||
if nestedTyp.Kind() != reflect.Struct {
|
||||
continue
|
||||
}
|
||||
if nestedTyp.Kind() == reflect.Struct {
|
||||
// 递归查找,找到有效cmd则立即返回
|
||||
if nestedCmd := getCmd(nestedTyp); len(nestedCmd) > 0 && nestedCmd[0] != defaultCmdValue {
|
||||
return nestedCmd
|
||||
}
|
||||
|
||||
nestedBinding, ok := findCmdBinding(nestedTyp, visiting)
|
||||
if !ok {
|
||||
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 []uint32{defaultCmdValue}
|
||||
return cmdBinding{}, false
|
||||
}
|
||||
|
||||
// parseCmdTagWithStructField 校验字段是否为TomeeHeader(值/指针)并解析cmd标签
|
||||
// 参数 field: 结构体字段元信息
|
||||
// 返回值: 解析后的cmd切片,非目标类型/解析失败返回错误
|
||||
func parseCmdTagWithStructField(field reflect.StructField) ([]uint32, error) {
|
||||
// 判断字段类型是否为 TomeeHeader 或 *TomeeHeader
|
||||
var isTomeeHeader bool
|
||||
switch {
|
||||
case field.Type == targetType: // 值类型
|
||||
isTomeeHeader = true
|
||||
case field.Type.Kind() == reflect.Ptr && field.Type.Elem() == targetType: // 指针类型
|
||||
isTomeeHeader = true
|
||||
default:
|
||||
isTomeeHeader = false
|
||||
// 返回值: 解析后的cmd切片,是否为目标类型,解析失败错误
|
||||
func parseCmdTagWithStructField(field reflect.StructField) ([]uint32, bool, error) {
|
||||
if field.Type != targetType && !(field.Type.Kind() == reflect.Ptr && field.Type.Elem() == targetType) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// 非目标类型返回错误
|
||||
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")
|
||||
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,减少第三方依赖且可控)
|
||||
parts := strings.Split(cmdStr, "|")
|
||||
result := make([]uint32, 0, len(parts))
|
||||
for idx, s := range parts {
|
||||
// 去除空白字符(兼容标签中意外的空格)
|
||||
s = strings.TrimSpace(s)
|
||||
result := make([]uint32, 0, strings.Count(cmdStr, "|")+1)
|
||||
remain := cmdStr
|
||||
for idx := 0; ; idx++ {
|
||||
part, next, found := strings.Cut(remain, "|")
|
||||
s := strings.TrimSpace(part)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
result = append(result, uint32(num))
|
||||
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
remain = next
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return result, true, nil
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"blazing/common/data"
|
||||
"blazing/common/socket/errorcode"
|
||||
"blazing/logic/service/egg"
|
||||
"blazing/logic/service/player"
|
||||
"blazing/modules/config/service"
|
||||
"blazing/modules/player/model"
|
||||
|
||||
"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) {
|
||||
|
||||
switch data1.EggNum {
|
||||
case 2:
|
||||
data1.EggNum = 5
|
||||
|
||||
case 3:
|
||||
data1.EggNum = 10
|
||||
}
|
||||
r := c.Service.Item.CheakItem(400501)
|
||||
if r < uint32(data1.EggNum) {
|
||||
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||
|
||||
}
|
||||
result = &egg.S2C_EGG_GAME_PLAY{ListInfo: []data.ItemInfo{}}
|
||||
if grand.Meet(int(data1.EggNum), 100) {
|
||||
r := service.NewPetRewardService().GetEgg()
|
||||
newPet := model.GenPetInfo(int(r.MonID), int(r.DV), int(r.Nature), int(r.Effect), int(r.Lv), nil)
|
||||
if grand.Meet(int(data1.EggNum), 100) {
|
||||
newPet.RandShiny()
|
||||
}
|
||||
c.Service.Pet.PetAdd(newPet)
|
||||
|
||||
result.HadTime = newPet.CatchTime
|
||||
result.PetID = newPet.ID
|
||||
}
|
||||
|
||||
items := service.NewItemService().GetEgg(data1.EggNum)
|
||||
for _, item := range items {
|
||||
if item.ItemId == 0 {
|
||||
continue
|
||||
}
|
||||
c.ItemAdd(item.ItemId, item.ItemCnt)
|
||||
result.ListInfo = append(result.ListInfo, data.ItemInfo{ItemId: item.ItemId, ItemCnt: item.ItemCnt})
|
||||
}
|
||||
|
||||
c.Service.Item.UPDATE(400501, int(-data1.EggNum))
|
||||
return
|
||||
|
||||
}
|
||||
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"`
|
||||
}
|
||||
74
logic/controller/action_扭蛋.go
Normal file
74
logic/controller/action_扭蛋.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"blazing/common/data"
|
||||
"blazing/common/socket/errorcode"
|
||||
"blazing/logic/service/common"
|
||||
|
||||
"blazing/logic/service/player"
|
||||
"blazing/modules/config/service"
|
||||
"blazing/modules/player/model"
|
||||
|
||||
"github.com/gogf/gf/v2/util/grand"
|
||||
)
|
||||
|
||||
// EggGamePlay 处理控制器请求。
|
||||
func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (result *S2C_EGG_GAME_PLAY, err errorcode.ErrorCode) {
|
||||
|
||||
switch data1.EggNum {
|
||||
case 2:
|
||||
data1.EggNum = 5
|
||||
|
||||
case 3:
|
||||
data1.EggNum = 10
|
||||
}
|
||||
r := c.Service.Item.CheakItem(400501)
|
||||
if data1.EggNum > 10 || data1.EggNum <= 0 {
|
||||
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||
}
|
||||
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) {
|
||||
r := service.NewPetRewardService().GetEgg()
|
||||
newPet := model.GenPetInfo(int(r.MonID), int(r.DV), int(r.Nature), int(r.Effect), int(r.Lv), nil, 0)
|
||||
if grand.Meet(1, 500) {
|
||||
newPet.RandomByWeightShiny()
|
||||
}
|
||||
|
||||
c.Service.Pet.PetAdd(newPet, 0)
|
||||
|
||||
result.HadTime = newPet.CatchTime
|
||||
result.PetID = newPet.ID
|
||||
}
|
||||
|
||||
items := service.NewItemService().GetEgg(int(data1.EggNum))
|
||||
addedItems := c.ItemAddBatch(items)
|
||||
for _, item := range addedItems {
|
||||
result.ListInfo = append(result.ListInfo, data.ItemInfo{ItemId: item.ItemId, ItemCnt: item.ItemCnt})
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"blazing/common/socket/errorcode"
|
||||
"blazing/logic/service/leiyi"
|
||||
"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) {
|
||||
result = &leiyi.S2C_LEIYI_TRAIN_GET_STATUS{}
|
||||
|
||||
for i := 0; i < 6; i++ {
|
||||
result.Status[i].Total = 10
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
39
logic/controller/activce_雷伊特训.go
Normal file
39
logic/controller/activce_雷伊特训.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"blazing/common/socket/errorcode"
|
||||
|
||||
"blazing/logic/service/common"
|
||||
"blazing/logic/service/player"
|
||||
)
|
||||
|
||||
// GetLeiyiTrainStatus 处理控制器请求。
|
||||
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++ {
|
||||
result.Status[i].Total = 10
|
||||
|
||||
}
|
||||
|
||||
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次数
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"blazing/common/socket/errorcode"
|
||||
"blazing/logic/service/pet"
|
||||
"blazing/logic/service/player"
|
||||
|
||||
"github.com/gogf/gf/v2/util/grand"
|
||||
)
|
||||
|
||||
// CatchPet 传送仓抓稀有宠物
|
||||
// data: 空输入结构
|
||||
// c: 当前玩家对象
|
||||
// 返回: 捕捉结果(消耗的EV值)和错误码
|
||||
func (h Controller) CatchPet(data *pet.C2S_9756, c *player.Player) (result *pet.S2C_9756, err errorcode.ErrorCode) {
|
||||
result = &pet.S2C_9756{
|
||||
UseEV: uint32(grand.N(1, 13)),
|
||||
}
|
||||
return
|
||||
}
|
||||
12
logic/controller/active_传送仓稀有.go
Normal file
12
logic/controller/active_传送仓稀有.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package controller
|
||||
|
||||
// CatchPet 传送仓抓稀有宠物
|
||||
// data: 空输入结构
|
||||
// c: 当前玩家对象
|
||||
// 返回: 捕捉结果(消耗的EV值)和错误码
|
||||
// func (h Controller) CatchPet(data *pet.C2S_9756, c *player.Player) (result *pet.S2C_9756, err errorcode.ErrorCode) {
|
||||
// result = &pet.S2C_9756{
|
||||
// UseEV: uint32(grand.N(1, 13)),
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
41
logic/controller/active_寒流枪.go
Normal file
41
logic/controller/active_寒流枪.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"blazing/common/socket/errorcode"
|
||||
"blazing/cool"
|
||||
"blazing/logic/service/common"
|
||||
"blazing/logic/service/fight"
|
||||
"blazing/logic/service/player"
|
||||
)
|
||||
|
||||
// CatchPet 传送仓抓稀有宠物
|
||||
// data: 空输入结构
|
||||
// c: 当前玩家对象
|
||||
// 返回: 捕捉结果(消耗的EV值)和错误码
|
||||
func (h Controller) HanLiuQiang(data *C2S_2608, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||
|
||||
if c.ItemAdd(100245, 1) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 准备战斗
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer c.FightC.Over(c, info.BattleOverReason.PlayerEscape)
|
||||
h.dispatchFightActionEnvelope(c, buildLegacyEscapeEnvelope())
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
defer c.FightC.ChangePet(c, data.CatchTime)
|
||||
h.dispatchFightActionEnvelope(c, buildLegacyChangeEnvelope(data))
|
||||
return nil, -1
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, -1
|
||||
}
|
||||
defer c.FightC.LoadPercent(c, int32(data.Percent))
|
||||
go c.FightC.LoadPercent(c, int32(data.Percent))
|
||||
return nil, -1
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
defer c.FightC.Chat(c, data.Message)
|
||||
h.dispatchFightActionEnvelope(c, buildChatEnvelope(data))
|
||||
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 (
|
||||
"blazing/common/socket/errorcode"
|
||||
"blazing/modules/config/service"
|
||||
"blazing/modules/player/model"
|
||||
|
||||
"blazing/logic/service/common"
|
||||
"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.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 = 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.WinnerId == c.GetInfo().UserID {
|
||||
c.Info.MessWin += 1
|
||||
c.MessWin(true)
|
||||
p.MessWin(false)
|
||||
|
||||
} else {
|
||||
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
|
||||
@@ -34,23 +70,48 @@ func (h Controller) PetMelee(data *fight.StartPetWarInboundInfo, c *player.Playe
|
||||
|
||||
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
|
||||
// ElementTypeNumbers 是控制器层共享变量。
|
||||
var ElementTypeNumbers = []int{1, 2, 3, 5, 11, 4, 6, 7, 9}
|
||||
|
||||
switch data.Type {
|
||||
case 5:
|
||||
c.Fightinfo.Mode = info.BattleMode.SINGLE_MODE
|
||||
case 6:
|
||||
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 = 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.WinnerId == c.GetInfo().UserID {
|
||||
c.Info.MonKingWin += 1
|
||||
} else {
|
||||
p.GetInfo().MonKingWin += 1
|
||||
switch data.Type {
|
||||
case 11:
|
||||
if foi.WinnerId == c.GetInfo().UserID {
|
||||
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 (
|
||||
"blazing/common/socket/errorcode"
|
||||
"blazing/modules/player/model"
|
||||
"sync/atomic"
|
||||
|
||||
"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) {
|
||||
if c.GetSpace().Owner.UserID == c.Info.UserID {
|
||||
func (h Controller) OnPlayerHandleFightInvite(data *HandleFightInviteInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||
if c.IsArenaPVPLocked() {
|
||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||
}
|
||||
if c.GetSpace().Owner.UserID == data.UserID {
|
||||
@@ -23,8 +24,9 @@ func (h Controller) OnPlayerHandleFightInvite(data *fight.HandleFightInviteInbou
|
||||
|
||||
}
|
||||
|
||||
if !c.CanFight() {
|
||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||
r := c.CanFight()
|
||||
if c.CanFight() != 0 {
|
||||
return nil, r
|
||||
}
|
||||
|
||||
//c.Fightinfo.Status = info.BattleMode.FIGHT_WITH_NPC
|
||||
@@ -53,7 +55,9 @@ func (h Controller) OnPlayerHandleFightInvite(data *fight.HandleFightInviteInbou
|
||||
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) {
|
||||
if c.GetSpace().Owner.UserID == c.Info.UserID {
|
||||
func (h Controller) OnPlayerInviteOtherFight(data *InviteToFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||
if c.IsArenaPVPLocked() {
|
||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||
}
|
||||
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
|
||||
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 (
|
||||
"blazing/common/socket/errorcode"
|
||||
"blazing/modules/player/model"
|
||||
"sync/atomic"
|
||||
|
||||
"blazing/logic/service/common"
|
||||
"blazing/logic/service/fight"
|
||||
"blazing/logic/service/fight/info"
|
||||
"blazing/logic/service/player"
|
||||
"blazing/logic/service/space"
|
||||
)
|
||||
|
||||
// ArenaSetOwner 处理玩家占据擂台的请求
|
||||
// public static const ARENA_SET_OWENR:uint = 2417;
|
||||
// 如果星际擂台上无人,站到星际擂台的包
|
||||
// 前端到后端无数据内容 空包
|
||||
// 后端到前端无数据内容 空包
|
||||
// ArenaSetOwner 都需要通过2419包广播更新擂台状态
|
||||
func (h Controller) ArenaSetOwner(data *fight.ARENA_SET_OWENR, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||
// ARENA_SET_OWENR 定义请求或响应数据结构。
|
||||
type ARENA_SET_OWENR struct {
|
||||
Head common.TomeeHeader `cmd:"2417" struc:"skip"`
|
||||
}
|
||||
|
||||
if !c.CanFight() {
|
||||
return nil, errorcode.ErrorCodes.ErrPokemonNotEligible
|
||||
// ArenaSetOwner 处理玩家占据擂台的请求
|
||||
// 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 //取消队列匹配
|
||||
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
|
||||
}
|
||||
|
||||
// ARENA_FIGHT_OWENR 定义请求或响应数据结构。
|
||||
type ARENA_FIGHT_OWENR struct {
|
||||
Head common.TomeeHeader `cmd:"2418" struc:"skip"`
|
||||
}
|
||||
|
||||
// ArenaFightOwner 挑战擂台的包
|
||||
// 前端到后端无数据内容 空包
|
||||
// 后端到前端无数据内容 空包
|
||||
// 还是后端主动发送2503的包给双方前端后 等待前端加载完毕 主动发送2404包通知后端开始战斗
|
||||
// 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() {
|
||||
return nil, errorcode.ErrorCodes.ErrNoEligiblePokemon
|
||||
r := c.CanFight()
|
||||
if r != 0 {
|
||||
return nil, r
|
||||
}
|
||||
|
||||
if c.Info.UserID == c.GetSpace().Owner.UserID {
|
||||
if c.IsArenaHost() {
|
||||
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
|
||||
}
|
||||
|
||||
if !c.GetSpace().Owner.ARENA_Player.CanFight() {
|
||||
if c.GetSpace().Owner.ARENA_Player.CanFight() != 0 {
|
||||
c.GetSpace().Owner.Set(c)
|
||||
c.GetSpace().Broadcast(c, 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.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 { //异常退出
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
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 后端到前端
|
||||
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
|
||||
return
|
||||
}
|
||||
|
||||
// ArenaUpfight 放弃擂台挑战的包
|
||||
// 前端到后端无数据内容
|
||||
// 后端到前端无数据内容
|
||||
// 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 { //说明已经有人了
|
||||
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;
|
||||
// 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()
|
||||
|
||||
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"`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user